diff --git a/refactor/baselines/vtt-thumbnail-contract-validation.json b/refactor/baselines/vtt-thumbnail-contract-validation.json new file mode 100644 index 000000000..793cd8184 --- /dev/null +++ b/refactor/baselines/vtt-thumbnail-contract-validation.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": 1, + "task": "PKG-VTT-THUMB-01", + "status": "done", + "parentCommit": "8c57d56f1732e745a4f18f51d735d5c44a203878", + "checks": [ + { + "command": "node --test refactor/scripts/vtt-thumbnail-contract.test.mjs refactor/scripts/risk-register.test.mjs refactor/scripts/contracts.test.mjs", + "result": "pass", + "tests": 100, + "packageContract": 91, + "log": "refactor/.cache/vtt-thumbnail01-targeted.log" + }, + { + "command": "yarn ci:check", + "result": "pass", + "tests": { + "unit": 1572, + "engineering": 14, + "baseline": 323, + "total": 1909 + }, + "log": "refactor/.cache/vtt-thumbnail01-ci.log", + "logSha256": "44cee18b3a0fa8da03ba6a8f2634b31459e2457b110841e41d10cad08a8dda5a" + }, + { + "command": "targeted eslint --fix then full CI lint --no-fix", + "result": "pass" + } + ], + "archiveScope": { + "versions": [ + "1.0.0", + "1.0.1", + "1.0.2", + "1.0.3", + "1.1.0" + ], + "members": 34, + "frozenInputs": 10, + "sourceOnly": "1.0.0 shipped source; actual main/legacy cannot load" + }, + "initialFinding": { + "tests": 79, + "pass": 43, + "fail": 36, + "cause": "Actual 1.0.1-1.0.3 bundles reject spaces after cue arrows; accepted fixtures are now explicit and separate assertions retain the standard-format failure", + "log": "refactor/.cache/vtt-thumbnail01-contract-initial.log" + }, + "limitations": [ + "Controlled host, not native browser or installed package acceptance", + "No supported core range inferred from Git associations", + "Production cleanup, parser errors, TS and consumer compatibility remain tasks 02-06" + ] +} diff --git a/refactor/baselines/vtt-thumbnail-contract.md b/refactor/baselines/vtt-thumbnail-contract.md new file mode 100644 index 000000000..e44a69a53 --- /dev/null +++ b/refactor/baselines/vtt-thumbnail-contract.md @@ -0,0 +1,60 @@ +# VTT-thumbnail 历史契约 + +`PKG-VTT-THUMB-01` 冻结五份实际 npm 归档、34 个成员和起点 +`8c57d56f1732e745a4f18f51d735d5c44a203878` 的十份工作区输入。 +详见 [归档](vtt-thumbnail-release.json)。本步不修改生产代码、类型或构建产物; +目标版本仍为 2.0.0。归档逐一验证 registry SHA-512、SHA-256 和成员哈希。 + +| 发布版本 | 实际运行入口 | 控件与集成 | Git 关联核心 | +| --- | --- | --- | --- | +| 1.0.0 | main/legacy 正则非法,加载即 SyntaxError | 随包源码使用 thumbnails、原生 mousemove/mouseleave;只能作为 source-only 夹具 | 5.1.2 | +| 1.0.1 | main/legacy 为 CJS default 对象 | thumbnails,setBar 和移动拖动定时隐藏 | 5.1.6 | +| 1.0.2 | 同上 | vtt-thumbnail,无新增 class | 5.1.7 | +| 1.0.3 | 同上 | vtt-thumbnail,art-control-thumbnails class | 5.1.7 | +| 1.1.0 | CJS 直接函数、ESM default、脚本全局 | 同 1.0.3 | 5.3.1 | + +这些 manifest 没有依赖、peerDependencies 或明确核心支持区间。Git 关联不是组合验收。 +最新归档六个成员与冻结工作区对应文件按 LF 一致。所有声明均误写同步返回结果; +option 对象必填,vtt 和 style 可选,旧声明使用 export= 和 namespace,新版 default。 + +## 实际可用行为 + +- 工厂/global 名 artplayerPluginVttThumbnail;调用工厂不发请求,注册后 fetch(vtt), + 等待 text/解析完成才添加控件,实际 Promise resolve 为仅含 name 的对象。 +- URL 在请求前读取,style 在 await 后读取,因此 style 替换在请求期间仍有效。 + style 使用原对象,position=top、index=20;省略 option 异步拒绝,省略 vtt 请求空字符串。 +- 时间向下取整,区间两端包含,重叠时第一个 cue 获胜;这可能使 0.9 秒的 cue 提前命中。 + sprite 坐标保留字符串,背景位置为负 x/y,按进度宽度夹住预览位置。 +- 相对图片地址按 VTT URL 最后一段之前的字符串拼接,保留 ../;绝对和根路径不重写。 +- setBar 的 hover 或带 event 的移动端 played 更新预览;桌面端边界隐藏。 + 移动拖动替换 500ms 隐藏计时器。1.0.0 随包源码则用原生鼠标事件和 hover 隐藏。 + +## 已复现差异与缺陷 + +1. 1.0.0 两个实际入口都有 `(?:?-->?)`,无法解析。不得以有效源码冒充已发布运行产物。 +2. 1.0.1–1.0.3 编译正则不接受箭头之后的空格,普通 `00:00.000 --> 00:05.000` + 会拒绝;其随包源码支持普通格式。测试的有效行为部分显式使用 compact 箭头, + 另有十二项测试分别锁定普通格式的成功或失败。不会悄悄替换 fetch 输入来掩盖问题。 + 初次测试 79 项中 36 项失败正是该差异;修正历史夹具归属并增加差异断言后 91 项通过。 +3. 当前解析器假设非空行从第二行起按 time/text 成对,没有检查 match、HTTP 状态, + 没有取消请求。NOTE、cue ID、坏坐标和失败响应需要 02 专门复现,不能称为已覆盖。 +4. 源码没有 destroy 监听或 abort;等待 fetch 后仍会安装控件,移动计时器和 setBar 无独立清理。 + 02 复现,03 划分请求、解析、视图和生命周期所有权并修复,05 验证真实核心行为。 +5. 旧 CJS `.default`、异步声明矛盾和旧控件名需要 04–06 逐项验证消费者; + 不因最新形状不同而直接删除旧兼容目标,也不在契约任务中臆造类型解决方案。 + +历史编译缺陷、泄漏不要求候选保留;正常调用的时序、几何、参数语义应保持兼容。 +涉及确实无法兼容的公开推导必须以实际旧消费者证据说明并取得决策。 + +## 验证边界与下一步 + +`node --test refactor/scripts/vtt-thumbnail-contract.test.mjs`:91 项,十二个行为夹具 +(八个实际 main/legacy,三个冻结工作区 source/main/legacy,一个 1.0.0 source-only), +另测实际最新 ESM、全局、归档、声明、源码一致性和 README/demo 文件。 +这些是可控 host/VM 证据,不证明浏览器渲染、实际安装或受支持核心版本范围。 + +真实样例为 docs/assets/example/vtt.thumbnail.js,使用本地 bbb-video.mp4、 +bbb-thumbnails.vtt、bbb-sprite.jpg。样例 VTT 使用普通带空格格式;文件核对不等于 +8082 示例或编辑器已通过。05/06 用 Chrome 或可用内置浏览器、真实旧/新核心、 +三引擎与适用实机、仓库外安装包和在线编辑器继续验证。Windows WebKit 不等于 Safari 实机。 +任务完成证据见 [验证记录](vtt-thumbnail-contract-validation.json)。 diff --git a/refactor/baselines/vtt-thumbnail-release.json b/refactor/baselines/vtt-thumbnail-release.json new file mode 100644 index 000000000..0d96d344a --- /dev/null +++ b/refactor/baselines/vtt-thumbnail-release.json @@ -0,0 +1,306 @@ +{ + "schemaVersion": 1, + "task": "PKG-VTT-THUMB-01", + "capturedAt": "2026-09-12T23:55:47.965Z", + "sourceCommit": "8c57d56f1732e745a4f18f51d735d5c44a203878", + "registryObservation": { + "url": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail", + "status": 200, + "sha256": "3c07418004b732ca2af5ada5625a78527a6fc21ee615074d4161b2bcbeecff4d", + "versions": [ + "1.0.0", + "1.0.1", + "1.0.2", + "1.0.3", + "1.1.0" + ], + "latest": "1.1.0", + "workspaceVersion": "1.1.0" + }, + "release": { + "name": "artplayer-plugin-vtt-thumbnail", + "version": "1.1.0", + "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/1.1.0", + "tarball": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/-/artplayer-plugin-vtt-thumbnail-1.1.0.tgz", + "integrity": "sha512-p5HIiaxBEN0CV4FWOWXCfy6hcQL59JuFIGUbQtVg9AcunPg2gJ1cJ8P0HEQ/ngH6tl1/puxrJIKydoAdX5L2dw==", + "sha256": "e2df36283538bbffc2c37fe28a8af0fe6fc0fa68a5f6cf93042929ee64d55765", + "archiveBytes": 3548, + "publishedAt": "2026-03-12T14:20:23.414Z", + "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96", + "files": { + "package/README.md": "aa10b8b0d740111dc02aa8a0957fbc5a465aadcd4fb792bfe75f0d8e5bd5f4a0", + "package/dist/artplayer-plugin-vtt-thumbnail.js": "2d8e22cbd191bdac5a6c711b641590c2f8a90ba1aae185dd0e02a00baf000f8d", + "package/dist/artplayer-plugin-vtt-thumbnail.legacy.js": "ecbd0abe12c537687fb058de552c328c71ced71d911e454d0e37887192e9a291", + "package/dist/artplayer-plugin-vtt-thumbnail.mjs": "413af5e4f28d47c659521332042a8347da357b17af39cf08c89ce3347897c618", + "package/package.json": "ed37ae39b73b67f6b6a9d43901ac11435f162e06d6a0a7b26c6614bcea0f9a38", + "package/types/artplayer-plugin-vtt-thumbnail.d.ts": "c2a010b219e10f56ba7357fd9646479045639b85f91be53405ad0ca1046970c6" + }, + "manifest": { + "name": "artplayer-plugin-vtt-thumbnail", + "version": "1.1.0", + "description": "vtt thumbnail plugin for ArtPlayer", + "author": "Harvey Zhao ", + "license": "MIT", + "homepage": "https://artplayer.org", + "repository": { + "type": "git", + "url": "git+https://github.com/zhw2590582/ArtPlayer.git" + }, + "bugs": { + "url": "https://github.com/zhw2590582/ArtPlayer/issues" + }, + "keywords": [ + "html5", + "video", + "player" + ], + "exports": { + ".": { + "types": "./types/artplayer-plugin-vtt-thumbnail.d.ts", + "import": "./dist/artplayer-plugin-vtt-thumbnail.mjs", + "require": "./dist/artplayer-plugin-vtt-thumbnail.js" + }, + "./legacy": { + "types": "./types/artplayer-plugin-vtt-thumbnail.d.ts", + "import": "./dist/artplayer-plugin-vtt-thumbnail.legacy.js", + "require": "./dist/artplayer-plugin-vtt-thumbnail.legacy.js" + } + }, + "main": "./dist/artplayer-plugin-vtt-thumbnail.js", + "module": "./dist/artplayer-plugin-vtt-thumbnail.mjs", + "types": "./types/artplayer-plugin-vtt-thumbnail.d.ts", + "legacy": "./dist/artplayer-plugin-vtt-thumbnail.legacy.js", + "browserslist": "last 1 Chrome version" + }, + "missingEntrypoints": {}, + "historicalCore": { + "version": "5.3.1", + "commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96", + "file": "packages/artplayer/package.json", + "sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c", + "meaning": "Git source association, not a declared or tested supported range." + } + }, + "previous": [ + { + "name": "artplayer-plugin-vtt-thumbnail", + "version": "1.0.0", + "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/1.0.0", + "tarball": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/-/artplayer-plugin-vtt-thumbnail-1.0.0.tgz", + "integrity": "sha512-tIUblchzZcYgxP7nUQxjr997ejuUWH6Xn8Qe1ls94k+Aoykv3Gq6H54mFpfvik4Cx2TqFER+Cq21bb+9h4nhjQ==", + "sha256": "18440510636529d76d97cd653433e65f795173e2e134d24caf0612845328dca1", + "archiveBytes": 8616, + "publishedAt": "2024-05-27T03:32:55.989Z", + "registryGitHead": "43309b72cff84344bd61f4de505aee0e10a83e6b", + "files": { + "package/README.md": "b678aa0bc562de5eb4cb3f9f9c528974368f77cc445c7ff6ccd8021c9edd4e1d", + "package/dist/artplayer-plugin-vtt-thumbnail.js": "b430236fbabdce0edb179658dc4c498b83f52ccb08a4a692f555d296443a3f61", + "package/dist/artplayer-plugin-vtt-thumbnail.legacy.js": "94b7ea6e9ff16830eb1200d091b09f515d04020eead8d18c14138a98577d9807", + "package/package.json": "e6bce38fc30f859a1edc14c29dbdb36d659993a5d41f725b561c4ef2b0a065c0", + "package/src/getVttArray.js": "0407f520a855724a11c5e47b621327edd2777f74f9c00e9b67537c756936a091", + "package/src/index.js": "378fdaf2a3010cdd0fe172901f1b81adb30437f13390dbb0c6fe3ad1297d3e48", + "package/types/artplayer-plugin-vtt-thumbnail.d.ts": "f3fb89d9e5c64215fb51722f25cce99e51baa3648e001fbab3fdef26de85efc4" + }, + "manifest": { + "license": "MIT", + "version": "1.0.0", + "name": "artplayer-plugin-vtt-thumbnail", + "types": "types/artplayer-plugin-vtt-thumbnail.d.ts", + "description": "vtt thumbnail plugin for ArtPlayer", + "main": "dist/artplayer-plugin-vtt-thumbnail.js", + "legacy": "dist/artplayer-plugin-vtt-thumbnail.legacy.js", + "author": "Harvey Zack ", + "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" + ] + }, + "missingEntrypoints": {}, + "historicalCore": { + "version": "5.1.2", + "commit": "43309b72cff84344bd61f4de505aee0e10a83e6b", + "file": "packages/artplayer/package.json", + "sha256LF": "aaf3ba9c7beaaea31170cc50e3958c0bb5d779ab4f90b6a213994ce8c9aace36", + "meaning": "Git source association, not a declared or tested supported range." + } + }, + { + "name": "artplayer-plugin-vtt-thumbnail", + "version": "1.0.1", + "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/1.0.1", + "tarball": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/-/artplayer-plugin-vtt-thumbnail-1.0.1.tgz", + "integrity": "sha512-t+xnYNYl0rhDBRJNOoTEamlQRbXJkIMGj01iuhcVO3OePjd/Ah52lJE+ZjMGD2Bso1BdQhWeYxQuZwGKhcf4nA==", + "sha256": "2827599516c48fa17c3e8c917164df8ea3f9a3a5ef2643e079fd0df73a171175", + "archiveBytes": 8656, + "publishedAt": "2024-06-15T11:55:58.930Z", + "registryGitHead": "74460a765e792d79296a7f8072e1159d2899b991", + "files": { + "package/README.md": "b678aa0bc562de5eb4cb3f9f9c528974368f77cc445c7ff6ccd8021c9edd4e1d", + "package/dist/artplayer-plugin-vtt-thumbnail.js": "d247f01b72f6bc51e3107006296ae6d198f4478a6d3dd350bc8741221a1ea5ea", + "package/dist/artplayer-plugin-vtt-thumbnail.legacy.js": "15bd66d9820d5517355dccd68178e9e3b64965504a98906c3473dbc62e744434", + "package/package.json": "3701442798961615144f6d4d02e7855e2933ce5879e97629b64f2a166b8ba3b3", + "package/src/getVttArray.js": "0407f520a855724a11c5e47b621327edd2777f74f9c00e9b67537c756936a091", + "package/src/index.js": "b30fb9370b5715511002562bb9fc84a35313a697b78fccd97bb5ebc26c1274b3", + "package/types/artplayer-plugin-vtt-thumbnail.d.ts": "f3fb89d9e5c64215fb51722f25cce99e51baa3648e001fbab3fdef26de85efc4" + }, + "manifest": { + "license": "MIT", + "version": "1.0.1", + "name": "artplayer-plugin-vtt-thumbnail", + "types": "types/artplayer-plugin-vtt-thumbnail.d.ts", + "description": "vtt thumbnail plugin for ArtPlayer", + "main": "dist/artplayer-plugin-vtt-thumbnail.js", + "legacy": "dist/artplayer-plugin-vtt-thumbnail.legacy.js", + "author": "Harvey Zack ", + "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" + ] + }, + "missingEntrypoints": {}, + "historicalCore": { + "version": "5.1.6", + "commit": "74460a765e792d79296a7f8072e1159d2899b991", + "file": "packages/artplayer/package.json", + "sha256LF": "412253a67616ed94c058a4eefdb5f0e7b863f696a92e5ac9e3e82c87ce5aec0d", + "meaning": "Git source association, not a declared or tested supported range." + } + }, + { + "name": "artplayer-plugin-vtt-thumbnail", + "version": "1.0.2", + "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/1.0.2", + "tarball": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/-/artplayer-plugin-vtt-thumbnail-1.0.2.tgz", + "integrity": "sha512-N7t8QjZUKfeQySWViQ/BdJ0cTBZ50N6WIhuv/bsuUGZNI2BAPlguaOxcVfa+tN2h1xBtbxmafXyA2s02IoiCVA==", + "sha256": "a2230637d991731cef5f5f133bcb93a2e41bacb8a1a361b60b5c901a267fcbf1", + "archiveBytes": 8661, + "publishedAt": "2024-08-31T03:49:59.580Z", + "registryGitHead": "c7299b926eb9ee2e5d0070b23621902118c17da6", + "files": { + "package/README.md": "b678aa0bc562de5eb4cb3f9f9c528974368f77cc445c7ff6ccd8021c9edd4e1d", + "package/dist/artplayer-plugin-vtt-thumbnail.js": "d163ee19b42ea0236a94f3eccb402292c5ba9987db03accf967027cc163192b1", + "package/dist/artplayer-plugin-vtt-thumbnail.legacy.js": "e11fc1fec96686f966477b8419405bebf86f5a0b6855f8c829feb43e8542879a", + "package/package.json": "2ddb70ed39f01191d5e1aa944f1e857153c40e1f1424e09d121b26d322bb5b05", + "package/src/getVttArray.js": "0407f520a855724a11c5e47b621327edd2777f74f9c00e9b67537c756936a091", + "package/src/index.js": "1be3ae540320b0b974a1ce8a62f5b403678791767df1f10c9d203b80e2c9f3e2", + "package/types/artplayer-plugin-vtt-thumbnail.d.ts": "f3fb89d9e5c64215fb51722f25cce99e51baa3648e001fbab3fdef26de85efc4" + }, + "manifest": { + "license": "MIT", + "version": "1.0.2", + "name": "artplayer-plugin-vtt-thumbnail", + "types": "types/artplayer-plugin-vtt-thumbnail.d.ts", + "description": "vtt thumbnail plugin for ArtPlayer", + "main": "dist/artplayer-plugin-vtt-thumbnail.js", + "legacy": "dist/artplayer-plugin-vtt-thumbnail.legacy.js", + "author": "Harvey Zack ", + "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" + ] + }, + "missingEntrypoints": {}, + "historicalCore": { + "version": "5.1.7", + "commit": "c7299b926eb9ee2e5d0070b23621902118c17da6", + "file": "packages/artplayer/package.json", + "sha256LF": "8bd532103099c367fb2c289e0d410f144fdeb00b1b62cf850762a24b61e63c8a", + "meaning": "Git source association, not a declared or tested supported range." + } + }, + { + "name": "artplayer-plugin-vtt-thumbnail", + "version": "1.0.3", + "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/1.0.3", + "tarball": "https://registry.npmjs.org/artplayer-plugin-vtt-thumbnail/-/artplayer-plugin-vtt-thumbnail-1.0.3.tgz", + "integrity": "sha512-JiwqpQsWUSf1MR+K1kHKsjKRPB4nPDNv80wCXbqYZWQ4/fpobN5J4jn8jZ19n6Dplm4OnIUZi5/huGWOgOcobA==", + "sha256": "286ba11db6adaeb85a4b3fef198af9a10ed0a548566d57ef7efa77c4b4f05f83", + "archiveBytes": 8698, + "publishedAt": "2024-08-31T04:02:06.368Z", + "registryGitHead": "44171beb3ad1735a3bb487605a9e540b789a671c", + "files": { + "package/README.md": "b678aa0bc562de5eb4cb3f9f9c528974368f77cc445c7ff6ccd8021c9edd4e1d", + "package/dist/artplayer-plugin-vtt-thumbnail.js": "2c5a97f61cc2c94c7684e7c0fc0bdf3cfad8e641a73cec8e534a1854754d8066", + "package/dist/artplayer-plugin-vtt-thumbnail.legacy.js": "ecb68c8cc66a06d5c0f18110bd375ff1dcf3b7e3574af205de3192e7d3932651", + "package/package.json": "fc5fd92b9b9b54dfd7aa025ba7896c2895e441796751df14ca90b81f5421d477", + "package/src/getVttArray.js": "0407f520a855724a11c5e47b621327edd2777f74f9c00e9b67537c756936a091", + "package/src/index.js": "284fc9f0bbd70ab58586b7d4f54c0fba17314bc908d5ff2c91f2c07db8a2ed81", + "package/types/artplayer-plugin-vtt-thumbnail.d.ts": "f3fb89d9e5c64215fb51722f25cce99e51baa3648e001fbab3fdef26de85efc4" + }, + "manifest": { + "license": "MIT", + "version": "1.0.3", + "name": "artplayer-plugin-vtt-thumbnail", + "types": "types/artplayer-plugin-vtt-thumbnail.d.ts", + "description": "vtt thumbnail plugin for ArtPlayer", + "main": "dist/artplayer-plugin-vtt-thumbnail.js", + "legacy": "dist/artplayer-plugin-vtt-thumbnail.legacy.js", + "author": "Harvey Zack ", + "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" + ] + }, + "missingEntrypoints": {}, + "historicalCore": { + "version": "5.1.7", + "commit": "44171beb3ad1735a3bb487605a9e540b789a671c", + "file": "packages/artplayer/package.json", + "sha256LF": "8bd532103099c367fb2c289e0d410f144fdeb00b1b62cf850762a24b61e63c8a", + "meaning": "Git source association, not a declared or tested supported range." + } + } + ], + "source": { + "packages/artplayer-plugin-vtt-thumbnail/.npmignore": "46063058e98be55dba5cf5f33d379e73eca048f4126e7af75f8011bab7f09145", + "packages/artplayer-plugin-vtt-thumbnail/README.md": "aa10b8b0d740111dc02aa8a0957fbc5a465aadcd4fb792bfe75f0d8e5bd5f4a0", + "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.js": "2d8e22cbd191bdac5a6c711b641590c2f8a90ba1aae185dd0e02a00baf000f8d", + "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.legacy.js": "ecbd0abe12c537687fb058de552c328c71ced71d911e454d0e37887192e9a291", + "packages/artplayer-plugin-vtt-thumbnail/dist/artplayer-plugin-vtt-thumbnail.mjs": "413af5e4f28d47c659521332042a8347da357b17af39cf08c89ce3347897c618", + "packages/artplayer-plugin-vtt-thumbnail/package.json": "ed37ae39b73b67f6b6a9d43901ac11435f162e06d6a0a7b26c6614bcea0f9a38", + "packages/artplayer-plugin-vtt-thumbnail/src/getVttArray.js": "5184d98a40017829f4c0b3c7514c22f866cea6bc9aa706e140127b694727d71b", + "packages/artplayer-plugin-vtt-thumbnail/src/index.js": "183c0f8f5cab033c3277c3d791b0f555ad5858aeb47c0b829d42487b7de921f2", + "packages/artplayer-plugin-vtt-thumbnail/types/artplayer-plugin-vtt-thumbnail.d.ts": "c2a010b219e10f56ba7357fd9646479045639b85f91be53405ad0ca1046970c6", + "docs/assets/example/vtt.thumbnail.js": "05465b0da63b23626a3032a9c1f44d3e78ed501a313aaed885ac232ab720ffee" + } +} diff --git a/refactor/changes/2026-09-13-PKG-VTT-THUMB-01-contract.md b/refactor/changes/2026-09-13-PKG-VTT-THUMB-01-contract.md new file mode 100644 index 000000000..603012d28 --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-VTT-THUMB-01-contract.md @@ -0,0 +1,14 @@ +# PKG-VTT-THUMB-01 发布和行为契约 + +冻结 1.0.0、1.0.1、1.0.2、1.0.3、1.1.0 五个实际归档,核验 34 成员和十个 Git 输入。 +新增可重跑验证器、可控 host、91 项历史契约测试,自动纳入现有 test:baseline。 +分别记录无效入口、编译正则和随包源码差异、控件名与 CJS 形状演进、异步声明错误。 +初次 43 通过/36 失败的日志保留;最终测试显式分开有效 compact 输入和普通格式差异。 + +发布索引从 20 增至 25(另有 22 工作区),没有把这些基线升级为最终兼容验收。 +新增五项风险交由 02–06 承接,均 open。本任务不修改生产实现或生成 dist,无需重建。 +无新依赖。源码结构、清理、严格 TS、真实核心/浏览器、安装与 demo 仍在后续任务。 + +完整范围见 [契约](../baselines/vtt-thumbnail-contract.md),实际命令和结果见 +[验证记录](../baselines/vtt-thumbnail-contract-validation.json)。回退此任务提交可撤销 +基线、测试及索引记录,不影响播放器运行行为。独立本地提交,不推送或发布。 diff --git a/refactor/contract-index.md b/refactor/contract-index.md index be3413af6..73aebea61 100644 --- a/refactor/contract-index.md +++ b/refactor/contract-index.md @@ -329,3 +329,8 @@ - PUB-artplayer-plugin-auto-thumbnail-1.1.0: artplayer-plugin-auto-thumbnail@1.1.0 (published-baseline);[来源](../refactor/baselines/auto-thumbnail-release.json) JSON pointer `/release`。 - PUB-artplayer-plugin-auto-thumbnail-1.0.1: artplayer-plugin-auto-thumbnail@1.0.1 (published-baseline);[来源](../refactor/baselines/auto-thumbnail-release.json) JSON pointer `/previous/0`。 - PUB-artplayer-plugin-auto-thumbnail-1.0.0: artplayer-plugin-auto-thumbnail@1.0.0 (published-baseline);[来源](../refactor/baselines/auto-thumbnail-release.json) JSON pointer `/previous/1`。 +- PUB-artplayer-plugin-vtt-thumbnail-1.1.0: artplayer-plugin-vtt-thumbnail@1.1.0 (published-baseline);[来源](../refactor/baselines/vtt-thumbnail-release.json) JSON pointer `/release`。 +- PUB-artplayer-plugin-vtt-thumbnail-1.0.0: artplayer-plugin-vtt-thumbnail@1.0.0 (published-baseline);[来源](../refactor/baselines/vtt-thumbnail-release.json) JSON pointer `/previous/0`。 +- PUB-artplayer-plugin-vtt-thumbnail-1.0.1: artplayer-plugin-vtt-thumbnail@1.0.1 (published-baseline);[来源](../refactor/baselines/vtt-thumbnail-release.json) JSON pointer `/previous/1`。 +- PUB-artplayer-plugin-vtt-thumbnail-1.0.2: artplayer-plugin-vtt-thumbnail@1.0.2 (published-baseline);[来源](../refactor/baselines/vtt-thumbnail-release.json) JSON pointer `/previous/2`。 +- PUB-artplayer-plugin-vtt-thumbnail-1.0.3: artplayer-plugin-vtt-thumbnail@1.0.3 (published-baseline);[来源](../refactor/baselines/vtt-thumbnail-release.json) JSON pointer `/previous/3`。 diff --git a/refactor/contract-policy.json b/refactor/contract-policy.json index c42e6da78..26b373120 100644 --- a/refactor/contract-policy.json +++ b/refactor/contract-policy.json @@ -436,10 +436,15 @@ "API-12": "PKG-VTT-THUMB-02" }, "versionIds": [ - "WS-artplayer-plugin-vtt-thumbnail" + "WS-artplayer-plugin-vtt-thumbnail", + "PUB-artplayer-plugin-vtt-thumbnail-1.1.0", + "PUB-artplayer-plugin-vtt-thumbnail-1.0.0", + "PUB-artplayer-plugin-vtt-thumbnail-1.0.1", + "PUB-artplayer-plugin-vtt-thumbnail-1.0.2", + "PUB-artplayer-plugin-vtt-thumbnail-1.0.3" ], "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" + "scopeReview": "Five verified actual npm archives; 1.0.0 main/legacy cannot parse, and 1.0.1–1.0.3 compiled timestamp parsing differs from their shipped source. Source-only fixtures and Git core associations are not installed or browser support evidence. See vtt-thumbnail-contract.md." }, { "name": "artplayer-proxy-canvas", @@ -912,6 +917,51 @@ "file": "refactor/baselines/auto-thumbnail-release.json", "pointer": "/previous/1", "archiveSha256": "0a907bc8dbf2ef773764645730309b2c95592f84b31f5c4f9cec636d29c0263e" + }, + { + "id": "PUB-artplayer-plugin-vtt-thumbnail-1.1.0", + "package": "artplayer-plugin-vtt-thumbnail", + "kind": "published-baseline", + "version": "1.1.0", + "file": "refactor/baselines/vtt-thumbnail-release.json", + "pointer": "/release", + "archiveSha256": "e2df36283538bbffc2c37fe28a8af0fe6fc0fa68a5f6cf93042929ee64d55765" + }, + { + "id": "PUB-artplayer-plugin-vtt-thumbnail-1.0.0", + "package": "artplayer-plugin-vtt-thumbnail", + "kind": "published-baseline", + "version": "1.0.0", + "file": "refactor/baselines/vtt-thumbnail-release.json", + "pointer": "/previous/0", + "archiveSha256": "18440510636529d76d97cd653433e65f795173e2e134d24caf0612845328dca1" + }, + { + "id": "PUB-artplayer-plugin-vtt-thumbnail-1.0.1", + "package": "artplayer-plugin-vtt-thumbnail", + "kind": "published-baseline", + "version": "1.0.1", + "file": "refactor/baselines/vtt-thumbnail-release.json", + "pointer": "/previous/1", + "archiveSha256": "2827599516c48fa17c3e8c917164df8ea3f9a3a5ef2643e079fd0df73a171175" + }, + { + "id": "PUB-artplayer-plugin-vtt-thumbnail-1.0.2", + "package": "artplayer-plugin-vtt-thumbnail", + "kind": "published-baseline", + "version": "1.0.2", + "file": "refactor/baselines/vtt-thumbnail-release.json", + "pointer": "/previous/2", + "archiveSha256": "a2230637d991731cef5f5f133bcb93a2e41bacb8a1a361b60b5c901a267fcbf1" + }, + { + "id": "PUB-artplayer-plugin-vtt-thumbnail-1.0.3", + "package": "artplayer-plugin-vtt-thumbnail", + "kind": "published-baseline", + "version": "1.0.3", + "file": "refactor/baselines/vtt-thumbnail-release.json", + "pointer": "/previous/3", + "archiveSha256": "286ba11db6adaeb85a4b3fef198af9a10ed0a548566d57ef7efa77c4b4f05f83" } ], "cases": [ diff --git a/refactor/plan.md b/refactor/plan.md index f3e71ffcd..f59dddd89 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。 -状态:todo 96 / doing 12 / blocked 0 / done 114 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 95 / doing 12 / blocked 0 / done 115 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -182,7 +182,7 @@ | ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 | | --- | --- | --- | --- | --- | --- | --- | -| PKG-VTT-THUMB-01 | artplayer-plugin-vtt-thumbnail
核对包契约与历史用法 | BASE-05 | vtt/style、异步返回、预览控件名与样式契约 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | todo | +| PKG-VTT-THUMB-01 | artplayer-plugin-vtt-thumbnail
核对包契约与历史用法 | BASE-05 | vtt/style、异步返回、预览控件名与样式契约 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done | | PKG-VTT-THUMB-02 | artplayer-plugin-vtt-thumbnail
建立特有行为与错误测试 | PKG-VTT-THUMB-01, ENG-03, ENG-05 | VTT 解析、xywh、相对 URL、时间边界、请求失败和定位 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | todo | | PKG-VTT-THUMB-03 | artplayer-plugin-vtt-thumbnail
整理内部职责与资源 | PKG-VTT-THUMB-02, CORE-09, CORE-19 | 纯解析/区间查找与 DOM、请求/定时器生命周期分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | todo | | PKG-VTT-THUMB-04 | artplayer-plugin-vtt-thumbnail
迁移自有源码和公开类型 | PKG-VTT-THUMB-03, ENG-04, ENG-06, CORE-07 | cue/rect/result 和参数可选性保持兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | todo | @@ -497,6 +497,7 @@ - PKG-AUTO-THUMB-01: [记录](baselines/auto-thumbnail-release.json) [记录](baselines/auto-thumbnail-contract.md) [记录](changes/2026-09-13-PKG-AUTO-THUMB-01-contract.md) [记录](baselines/auto-thumbnail-contract-validation.json) - PKG-AUTO-THUMB-02: [记录](baselines/auto-thumbnail-failures.md) [记录](baselines/auto-thumbnail-failures-validation.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-02-failures.md) - PKG-AUTO-THUMB-03: [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-lifecycle-checkpoint.md) [记录](baselines/auto-thumbnail-lifecycle-checkpoint.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md) [记录](baselines/auto-thumbnail-hidden-renderer.json) [记录](baselines/auto-thumbnail-timeline-media.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md) [记录](baselines/auto-thumbnail-frame-presentation.json) +- PKG-VTT-THUMB-01: [记录](baselines/vtt-thumbnail-release.json) [记录](baselines/vtt-thumbnail-contract.md) [记录](changes/2026-09-13-PKG-VTT-THUMB-01-contract.md) - PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md) - PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md) - PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md) diff --git a/refactor/progress.md b/refactor/progress.md index 867e238b8..9908038c8 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,15 @@ # 进度与证据 +## PKG-VTT-THUMB-01 历史契约完成 + +五份真实 npm 归档、34 成员、十份 Git 输入已冻结,91 项包契约及 9 项索引/风险检查通过。 +完整 CI 1909 项(1572 单元、14 工程、323 基线)通过。明确区分 1.0.0 无效运行入口、 +1.0.1–1.0.3 编译解析差异与有效随包源码;当前生产实现未改动。 +见[契约](baselines/vtt-thumbnail-contract.md)、[验证](baselines/vtt-thumbnail-contract-validation.json)、 +[变更](changes/2026-09-13-PKG-VTT-THUMB-01-contract.md)。五项风险保持 open,下一步 02 复现失败/销毁。 +222 项:115 done、12 doing、95 todo;本任务独立本地提交,不推送、不发布。 + + ## PKG-AUTO-THUMB-03 原生帧就绪检查点(doing) 新增frames模块,对支持的浏览器同时等待seek完成与原生帧呈现,补齐取消、超时、 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 749f33a1e..7cbcbb493 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -189,3 +189,8 @@ | AUTO-THUMB-LIFE-01 | open / 已复现 | Extraction tasks, decoder handlers and final object URLs lack source-switch and destroy ownership | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 | | AUTO-THUMB-ENCODE-01 | open / 已复现 | Unserialized encodes include blank output and lack media/canvas/input failure handling | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 | | AUTO-THUMB-PIXEL-01 | open / 已复现 | Historical auto-thumbnail yields black sampled JPEG cells on Windows WebKit while played native video and static JPEG controls yield color | PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 | +| VTT-THUMB-DIST-01 | open / 已复现 | Historical 1.0.0 main/legacy contain invalid regular expressions and cannot load | PKG-VTT-THUMB-06 | +| VTT-THUMB-PARSE-01 | open / 已复现 | Historical compiled arrows differ from shipped source; parsing floors bounds and assumes alternating lines | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03 | +| VTT-THUMB-LIFE-01 | open / 源码/产物事实 | Fetch completion, setBar listeners and mobile timers have no destroy ownership | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-05 | +| VTT-THUMB-TYPE-01 | open / 已复现 | Published declarations falsely describe async registration as a synchronous result | PKG-VTT-THUMB-04 | +| VTT-THUMB-EXPORT-01 | open / 已复现 | Older CommonJS default objects and thumbnails control names differ from latest direct export and vtt-thumbnail name | PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 | diff --git a/refactor/risks.json b/refactor/risks.json index 5a717fd39..e3ca481d5 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4120,6 +4120,91 @@ "compatibleResolution": "Establish actual decoded-frame readiness for the existing timeline/grid and preserve silent independent extraction; do not waive colored pixel checks as unsupported media.", "closureCriteria": "Candidate source and artifacts generate usable sampled pixels on native browser controls with lifecycle checks; supported Safari and core combinations remain explicit final gates.", "workspaceState": "The frames module now waits for native presentation and seek completion on capable browsers. Chromium/Firefox source/main/legacy assert all five cells including first purple frame, second red cell, legitimate black and presentation times. Callback-less Windows WebKit first two cells remain diagnostic, later cells asserted; physical Safari/core combinations remain open. 84 native lifecycle/pixel cases passed with explicit held native-frame vs Blob-fallback paths; 19 new Node cases fail 17 ways on prior checkpoint and all 52 current cases pass per candidate. Risk and task03 remain open." + }, + { + "id": "VTT-THUMB-DIST-01", + "title": "Historical 1.0.0 main/legacy contain invalid regular expressions and cannot load", + "confirmation": "reproduced", + "status": "open", + "owners": [ + "PKG-VTT-THUMB-06" + ], + "evidence": [ + "refactor/baselines/vtt-thumbnail-release.json", + "refactor/baselines/vtt-thumbnail-contract.md", + "refactor/scripts/vtt-thumbnail-contract.test.mjs" + ], + "compatibleResolution": "Preserve archive evidence without reproducing broken entry syntax.", + "closureCriteria": "Candidate actual installed main/legacy/ESM/types/global entries work and historical deep paths are reviewed." + }, + { + "id": "VTT-THUMB-PARSE-01", + "title": "Historical compiled arrows differ from shipped source; parsing floors bounds and assumes alternating lines", + "confirmation": "reproduced", + "status": "open", + "owners": [ + "PKG-VTT-THUMB-02", + "PKG-VTT-THUMB-03" + ], + "evidence": [ + "refactor/baselines/vtt-thumbnail-release.json", + "refactor/baselines/vtt-thumbnail-contract.md", + "refactor/scripts/vtt-thumbnail-contract.test.mjs" + ], + "compatibleResolution": "Preserve valid cue selection and geometry; test malformed data and standard VTT separately from old compiler defects.", + "closureCriteria": "Parser fixtures include malformed, empty, real demo VTT, fractional and shared bounds, relative URLs and historical accepted formats." + }, + { + "id": "VTT-THUMB-LIFE-01", + "title": "Fetch completion, setBar listeners and mobile timers have no destroy ownership", + "confirmation": "source-observed", + "status": "open", + "owners": [ + "PKG-VTT-THUMB-02", + "PKG-VTT-THUMB-03", + "PKG-VTT-THUMB-05" + ], + "evidence": [ + "refactor/baselines/vtt-thumbnail-release.json", + "refactor/baselines/vtt-thumbnail-contract.md", + "refactor/scripts/vtt-thumbnail-contract.test.mjs" + ], + "compatibleResolution": "Own requests, listeners, timers and controls per registration without stale writes or cross-instance cleanup.", + "closureCriteria": "Deferred request, destroy, failure, timer, repeated registration and native core scenarios demonstrate cleanup." + }, + { + "id": "VTT-THUMB-TYPE-01", + "title": "Published declarations falsely describe async registration as a synchronous result", + "confirmation": "reproduced", + "status": "open", + "owners": [ + "PKG-VTT-THUMB-04" + ], + "evidence": [ + "refactor/baselines/vtt-thumbnail-release.json", + "refactor/baselines/vtt-thumbnail-contract.md", + "refactor/scripts/vtt-thumbnail-contract.test.mjs" + ], + "compatibleResolution": "Preserve valid consumers and actual async runtime, with explicit decisions for irreconcilable inference changes.", + "closureCriteria": "Isolated old/new compiler and installed consumers verify options, styles, callback and result forms." + }, + { + "id": "VTT-THUMB-EXPORT-01", + "title": "Older CommonJS default objects and thumbnails control names differ from latest direct export and vtt-thumbnail name", + "confirmation": "reproduced", + "status": "open", + "owners": [ + "PKG-VTT-THUMB-04", + "PKG-VTT-THUMB-05", + "PKG-VTT-THUMB-06" + ], + "evidence": [ + "refactor/baselines/vtt-thumbnail-release.json", + "refactor/baselines/vtt-thumbnail-contract.md", + "refactor/scripts/vtt-thumbnail-contract.test.mjs" + ], + "compatibleResolution": "Validate historical import forms and control integrations before selecting compatibility adapters.", + "closureCriteria": "Actual package consumers and old/new cores prove supported forms; unresolved differences stay explicit." } ] } diff --git a/refactor/scripts/contracts.test.mjs b/refactor/scripts/contracts.test.mjs index 0b788e5ec..01160e145 100644 --- a/refactor/scripts/contracts.test.mjs +++ b/refactor/scripts/contracts.test.mjs @@ -44,7 +44,8 @@ function fixture(t) { test('Actual contract model assigns all 264 family rows and separates exact published versions from workspace snapshots', () => { assert.equal(actual.policy.packages.length, 22) assert.equal(actual.families.length, 12) - assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 20) + assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 25) + assert.deepEqual(actual.policy.versions.filter(version => version.kind === 'published-baseline' && version.package === 'artplayer-plugin-vtt-thumbnail').map(version => version.version).sort(), ['1.0.0', '1.0.1', '1.0.2', '1.0.3', '1.1.0']) assert.deepEqual(actual.policy.versions.filter(version => version.kind === 'published-baseline' && version.package === 'artplayer-plugin-auto-thumbnail').map(version => version.version).sort(), ['1.0.0', '1.0.1', '1.1.0']) 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) diff --git a/refactor/scripts/vtt-thumbnail-contract.mjs b/refactor/scripts/vtt-thumbnail-contract.mjs new file mode 100644 index 000000000..a5554d6b2 --- /dev/null +++ b/refactor/scripts/vtt-thumbnail-contract.mjs @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import vm from 'node:vm' +import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs' + +export async function verifyVttThumbnailContract() { + const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/vtt-thumbnail-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) + const missing = Object.fromEntries(['main', 'module', 'types', 'legacy'].filter(field => release.manifest[field] && !Object.hasOwn(release.files, `package/${release.manifest[field].replace(/^\.\//, '')}`)).map(field => [field, release.manifest[field]])) + assert.deepEqual(missing, release.missingEntrypoints) + if (release.version === '1.0.0') { + for (const field of ['main', 'legacy']) { + const code = readMember(archive, `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() + assert.throws(() => new vm.Script(code), error => error.name === 'SyntaxError' && /Invalid regular expression/.test(error.message), 'The actual 1.0.0 bundles cannot parse; never replace them with source-only fixtures') + } + } + 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' }).replaceAll('\r\n', '\n') + assert.equal(hash(source), expected, `VTT-thumbnail frozen source: ${file}`) + sources.set(file, source) + } + return { baseline, archives, sources } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + await verifyVttThumbnailContract() + console.log('VTT-thumbnail verified: 5 actual npm archives, 34 members, 10 frozen inputs; 1.0.0 main/legacy syntax failures remain explicit') +} diff --git a/refactor/scripts/vtt-thumbnail-contract.test.mjs b/refactor/scripts/vtt-thumbnail-contract.test.mjs new file mode 100644 index 000000000..ca58ec5ce --- /dev/null +++ b/refactor/scripts/vtt-thumbnail-contract.test.mjs @@ -0,0 +1,232 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +// eslint-disable-next-line test/no-import-node-test -- Node frozen release-contract runner. +import test from 'node:test' +import vm from 'node:vm' +import { acceptedVttText, vttText, vttThumbnailEnvironment, vttThumbnailHistorical } from '../../test/helpers/vtt-thumbnail.js' +import { readMember } from './releases.mjs' +import { verifyVttThumbnailContract } from './vtt-thumbnail-contract.mjs' + +const contract = await verifyVttThumbnailContract() +const implementations = await vttThumbnailHistorical() +const name = 'artplayer-plugin-vtt-thumbnail' + +test('VTT-thumbnail freezes five actual archives, 34 members, ten source inputs and separate Git core associations', () => { + const releases = [contract.baseline.release, ...contract.baseline.previous] + assert.deepEqual(releases.map(release => release.version), ['1.1.0', '1.0.0', '1.0.1', '1.0.2', '1.0.3']) + assert.equal(releases.reduce((sum, release) => sum + Object.keys(release.files).length, 0), 34) + assert.equal(Object.keys(contract.baseline.source).length, 10) + assert.deepEqual(releases.map(release => release.historicalCore.version), ['5.3.1', '5.1.2', '5.1.6', '5.1.7', '5.1.7']) + for (const release of releases) { + assert.deepEqual(release.missingEntrypoints, {}) + assert.equal(release.manifest.dependencies, undefined) + assert.equal(release.manifest.peerDependencies, undefined) + } +}) + +test('VTT-thumbnail 1.0.0 actual main and legacy cannot parse; its valid shipped source stays source-only', () => { + const release = contract.baseline.previous[0] + for (const field of ['main', 'legacy']) { + const code = readMember(contract.archives.get('1.0.0'), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() + assert.throws(() => new vm.Script(code), error => error.name === 'SyntaxError' && /Nothing to repeat/.test(error.message)) + assert(code.includes('(?:?-->?)')) + } + const source = readMember(contract.archives.get('1.0.0'), 'package/src/getVttArray.js').toString() + assert(source.includes('(?: ?--> ?)')) + assert(implementations.some(item => item.name === 'published-1.0.0-source-only')) + assert(!implementations.some(item => item.name === 'published-1.0.0-main')) +}) + +test('VTT-thumbnail historical declarations omit Promise while retaining optional vtt/style and required option object', () => { + for (const release of [contract.baseline.release, ...contract.baseline.previous]) { + const source = readMember(contract.archives.get(release.version), `package/${release.manifest.types.replace(/^\.\//, '')}`).toString() + assert.match(source, /\(option: \{ vtt\?: string/) + assert.match(source, /style\?: Partial/) + assert.match(source, /name: ['"]artplayerPluginVttThumbnail['"]/) + assert.doesNotMatch(source, /Promise { + for (const implementation of implementations.filter(item => !item.name.includes('source'))) { + const env = vttThumbnailEnvironment(implementation, { text: acceptedVttText(implementation) }) + assert.equal(typeof env.factory, 'function') + assert.equal(env.requests.length, 0) + assert.equal(typeof env.exported, implementation.version.startsWith('1.0.') ? 'object' : 'function') + if (typeof env.exported === 'object') + assert.deepEqual(Object.keys(env.exported), ['default']) + const script = vttThumbnailEnvironment(implementation, { script: true }) + assert.equal(typeof script.factory, 'function') + assert.equal(script.requests.length, 0) + } +}) + +test('VTT-thumbnail actual latest ESM modules expose default-only factories and asynchronously register after fetch', async () => { + const codes = [readMember(contract.archives.get('1.1.0'), `package/${contract.baseline.release.manifest.module.replace(/^\.\//, '')}`).toString(), contract.sources.get(`packages/${name}/dist/${name}.mjs`)] + const original = globalThis.fetch + try { + for (const code of codes) { + const requests = [] + globalThis.fetch = async (url) => { + requests.push(url) + return { text: async () => vttText } + } + const exported = await import(`data:text/javascript,${encodeURIComponent(code)}`) + assert.deepEqual(Object.keys(exported), ['default']) + const env = vttThumbnailEnvironment(implementations[0]) + const result = exported.default({ vtt: 'sprite.vtt' })(env.art) + assert.equal(result.name, undefined) + assert.equal(typeof result.then, 'function') + assert.deepEqual(await result, { name: 'artplayerPluginVttThumbnail' }) + assert.deepEqual(requests, ['sprite.vtt']) + } + } + finally { globalThis.fetch = original } +}) + +test('VTT-thumbnail latest published members match their separately frozen workspace files after line-ending normalization', () => { + for (const member of Object.keys(contract.baseline.release.files)) { + const published = readMember(contract.archives.get('1.1.0'), member).toString().replaceAll('\r\n', '\n') + assert.equal(published, contract.sources.get(`packages/${name}/${member.slice('package/'.length)}`)) + } +}) + +test('VTT-thumbnail README and example select the VTT demo and its actual local sample assets', () => { + for (const release of [contract.baseline.release, ...contract.baseline.previous]) { + const readme = readMember(contract.archives.get(release.version), 'package/README.md').toString() + assert.match(readme, /example=vtt\.thumbnail/) + } + const example = contract.sources.get('docs/assets/example/vtt.thumbnail.js') + assert.match(example, /artplayerPluginVttThumbnail\(\{/) + for (const file of ['bbb-video.mp4', 'bbb-thumbnails.vtt']) { + assert(example.includes(`/assets/sample/${file}`)) + assert(fs.existsSync(`docs/assets/sample/${file}`)) + } +}) + +for (const implementation of implementations) { + test(`VTT-thumbnail ${implementation.name}: registration fetches once and adds controls only after awaiting text`, async () => { + const env = vttThumbnailEnvironment(implementation, { deferred: true, text: acceptedVttText(implementation) }) + const option = { vtt: 'https://cdn.test/folder/cues.vtt', style: { opacity: '0.8' } } + const register = env.factory(option) + assert.equal(env.requests.length, 0) + const result = register(env.art) + assert.equal(typeof result.then, 'function') + assert.equal(result.name, undefined) + assert.deepEqual(env.requests, [[option.vtt]]) + assert.equal(env.controls.length, 0) + assert.equal(env.listeners.size, 0) + env.resolve() + assert.deepEqual({ ...await result }, { name: 'artplayerPluginVttThumbnail' }) + assert.equal(env.controls.length, 1) + assert.equal(env.controls[0].name, ['mouse-source-only', 'old-control'].includes(implementation.profile) ? 'thumbnails' : 'vtt-thumbnail') + assert.equal(env.controls[0].position, 'top') + assert.equal(env.controls[0].index, 20) + assert.equal(env.controls[0].style, option.style) + assert.equal(env.classes.has('art-control-thumbnails'), implementation.profile === 'current') + }) + + test(`VTT-thumbnail ${implementation.name}: floored inclusive cue bounds select the first matching sprite and retain geometry`, async () => { + const env = vttThumbnailEnvironment(implementation, { text: acceptedVttText(implementation) }) + await env.factory({ vtt: 'https://cdn.test/folder/cues.vtt' })(env.art) + await env.hover(0.05) + assert.equal(env.styles.left, 0) + assert.equal(env.styles.backgroundImage, 'url(https://cdn.test/folder/sheet.jpg)') + assert.equal(env.styles.backgroundPosition, '-10px -20px') + assert.equal(env.styles.height, '45px') + assert.equal(env.styles.width, '80px') + await env.hover(0.5) + assert.equal(env.styles.backgroundImage, 'url(https://cdn.test/folder/sheet.jpg)', 'At the shared floored boundary the earlier inclusive cue wins') + assert.equal(env.styles.left, '60px') + await env.hover(0.51) + assert.equal(env.styles.backgroundImage, 'url(https://cdn.test/folder/second.jpg)') + assert.equal(env.styles.backgroundPosition, '-90px -65px') + await env.hover(0.99) + assert.equal(env.styles.left, '120px') + assert.equal(env.styles.display, implementation.profile === 'mouse-source-only' ? 'block' : 'flex') + }) + + test(`VTT-thumbnail ${implementation.name}: style is read after fetch while the VTT URL is captured before it`, async () => { + const env = vttThumbnailEnvironment(implementation, { deferred: true, text: acceptedVttText(implementation) }) + const options = { vtt: '/original.vtt', style: { opacity: '0.1' } } + const result = env.factory(options)(env.art) + options.vtt = '/later.vtt' + options.style = { opacity: '0.9' } + env.resolve() + await result + assert.deepEqual(env.requests, [['/original.vtt']]) + assert.equal(env.controls[0].style, options.style) + }) + + test(`VTT-thumbnail ${implementation.name}: image URLs preserve literal joining and absolute forms`, async () => { + for (const [image, expected] of [['../sprite.jpg', 'https://cdn.test/folder/../sprite.jpg'], ['/sprite.jpg', '/sprite.jpg'], ['https://img.test/s.jpg', 'https://img.test/s.jpg'], ['//img.test/s.jpg', '//img.test/s.jpg']]) { + const env = vttThumbnailEnvironment(implementation, { text: acceptedVttText(implementation, `WEBVTT\n\n00:00:00.000 --> 00:00:10.000\n${image}#xywh=1,2,80,45\n`) }) + await env.factory({ vtt: 'https://cdn.test/folder/cues.vtt' })(env.art) + await env.hover(0.5) + assert.equal(env.styles.backgroundImage, `url(${expected})`) + } + }) + + test(`VTT-thumbnail ${implementation.name}: omitted option rejects asynchronously while omitted vtt fetches the empty string`, async () => { + const env = vttThumbnailEnvironment(implementation, { text: acceptedVttText(implementation) }) + const result = env.factory()(env.art) + assert.equal(typeof result.then, 'function') + await assert.rejects(result, error => error.name === 'TypeError') + assert.equal(env.requests.length, 0) + await env.factory({})(env.art) + assert.deepEqual(env.requests, [['']]) + }) + + if (implementation.profile !== 'mouse-source-only') { + test(`VTT-thumbnail ${implementation.name}: setBar handles mobile played drags with a replaceable 500ms hide timer`, async () => { + const env = vttThumbnailEnvironment(implementation, { mobile: true, text: acceptedVttText(implementation) }) + await env.factory({ vtt: '/cues.vtt' })(env.art) + await env.hover(0.3, 'played', null) + assert.deepEqual(env.styles, {}) + await env.hover(0.3, 'played', {}) + assert.equal(env.styles.display, 'flex') + assert.equal(env.timers.size, 1) + await env.hover(0.7, 'played', {}) + assert.equal(env.timers.size, 1) + const timer = [...env.timers.values()][0] + assert.equal(timer.delay, 500) + timer.callback() + assert.equal(env.styles.display, 'none') + }) + } +} + +test('VTT-thumbnail 1.0.0 source-only retains native mousemove/mouseleave and hover-hide integration', async () => { + const implementation = implementations.find(item => item.profile === 'mouse-source-only') + const env = vttThumbnailEnvironment(implementation, { text: acceptedVttText(implementation) }) + await env.factory({})(env.art) + assert.deepEqual([...env.proxies.keys()], ['mousemove', 'mouseleave']) + assert.deepEqual([...env.listeners.keys()], ['hover']) + await env.hover(0) + assert.equal(env.styles.display, 'block') + await env.emit('hover', false) + assert.equal(env.styles.display, 'none') + await env.hover(1) + assert.equal(env.styles.display, 'block') + env.proxies.get('mouseleave')[0]() + assert.equal(env.styles.display, 'none') +}) + +for (const implementation of implementations) { + test(`VTT-thumbnail ${implementation.name}: standard spaced arrows remain distinct from historical compact-only bundles`, async () => { + const env = vttThumbnailEnvironment(implementation, { text: vttText }) + const result = env.factory({ vtt: '/cues.vtt' })(env.art) + if (/^published-1\.0\.[123]-(?:main|legacy)$/.test(implementation.name)) { + await assert.rejects(result, error => error.name === 'TypeError') + assert.equal(env.controls.length, 0) + } + else { + assert.equal((await result).name, 'artplayerPluginVttThumbnail') + await env.hover(0.3) + assert.equal(env.styles.backgroundImage, 'url(/sheet.jpg)') + } + }) +} diff --git a/refactor/tasks.json b/refactor/tasks.json index 620a55416..931af6e15 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1854,11 +1854,15 @@ "dependsOn": [ "BASE-05" ], - "status": "todo", + "status": "done", "risk": "M", "deliverable": "vtt/style、异步返回、预览控件名与样式契约", "acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结", - "evidence": [] + "evidence": [ + "baselines/vtt-thumbnail-release.json", + "baselines/vtt-thumbnail-contract.md", + "changes/2026-09-13-PKG-VTT-THUMB-01-contract.md" + ] }, { "id": "PKG-VTT-THUMB-02", diff --git a/test/helpers/vtt-thumbnail.js b/test/helpers/vtt-thumbnail.js new file mode 100644 index 000000000..0946ca723 --- /dev/null +++ b/test/helpers/vtt-thumbnail.js @@ -0,0 +1,123 @@ +import vm from 'node:vm' +import { transform } from 'esbuild' +import { readMember } from '../../refactor/scripts/releases.mjs' +import { verifyVttThumbnailContract } from '../../refactor/scripts/vtt-thumbnail-contract.mjs' + +export async function vttThumbnailHistorical() { + const { baseline, archives, sources } = await verifyVttThumbnailContract() + const name = 'artplayer-plugin-vtt-thumbnail' + const implementations = [] + for (const release of [baseline.release, ...baseline.previous]) { + if (release.version === '1.0.0') + continue + for (const field of ['main', 'legacy']) { + implementations.push({ name: `published-${release.version}-${field}`, version: release.version, profile: release.version === '1.0.1' ? 'old-control' : release.version === '1.0.2' ? 'no-class' : 'current', code: readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() }) + } + } + for (const version of ['1.0.0', 'workspace']) { + const source = file => version === 'workspace' ? sources.get(`packages/${name}/src/${file}`) : readMember(archives.get(version), `package/src/${file}`).toString() + const code = `${source('getVttArray.js').replace('export default ', '')}\n${source('index.js').replace(/^import getVttArray from ['"].\/getVttArray['"];?\s*$/m, '')}` + implementations.push({ name: version === 'workspace' ? 'frozen-workspace-source' : 'published-1.0.0-source-only', version, profile: version === '1.0.0' ? 'mouse-source-only' : 'current', code: (await transform(code, { format: 'cjs', target: 'es2020' })).code }) + } + for (const suffix of ['js', 'legacy.js']) + implementations.push({ name: `frozen-workspace-${suffix}`, version: 'workspace', profile: 'current', code: sources.get(`packages/${name}/dist/${name}.${suffix}`) }) + return implementations +} + +export const vttText = 'WEBVTT\n\n00:00:00.900 --> 00:00:05.900\nsheet.jpg#xywh=10,20,80,45\n\n00:00:05.100 --> 00:00:10.999\nsecond.jpg#xywh=90,65,80,45\n' + +// Only the 1.0.1–1.0.3 compiled artifacts require compact cue arrows. +export function acceptedVttText(implementation, text = vttText) { + return /^published-1\.0\.[123]-(?:main|legacy)$/.test(implementation.name) + ? text.replaceAll(' --> ', '-->') + : text +} + +export function vttThumbnailEnvironment(implementation, { script = false, mobile = false, text = vttText, deferred = false } = {}) { + const requests = [] + const controls = [] + const listeners = new Map() + const proxies = new Map() + const timers = new Map() + const classes = new Set() + const styles = {} + let nextTimer = 0 + let resolveFetch + let rejectFetch + const response = source => ({ ok: true, status: 200, text: async () => source }) + const pending = deferred + ? new Promise((resolve, reject) => { + resolveFetch = resolve + rejectFetch = reject + }) + : null + const progress = { clientWidth: 200, getBoundingClientRect: () => ({ left: 20 }) } + const control = { styles } + const art = { + constructor: { utils: { + isMobile: mobile, + clamp: (value, min, max) => Math.min(max, Math.max(min, value)), + setStyle(element, key, value) { element.styles[key] = value }, + addClass(element, name) { classes.add(name) }, + } }, + duration: 10, + template: { $progress: progress }, + events: { proxy(element, name, callback) { + if (!proxies.has(name)) + proxies.set(name, []) + proxies.get(name).push(callback) + } }, + controls: { add(option) { + controls.push(option) + option.mounted(control) + } }, + on(name, callback) { + if (!listeners.has(name)) + listeners.set(name, []) + listeners.get(name).push(callback) + return this + }, + off(name, callback) { listeners.set(name, (listeners.get(name) || []).filter(item => item !== callback)) }, + } + const box = { + window: {}, + fetch(...args) { + requests.push(args) + return pending || Promise.resolve(response(text)) + }, + setTimeout(callback, delay) { + const id = nextTimer++ + timers.set(id, { callback, delay }) + return id + }, + clearTimeout(id) { timers.delete(id) }, + } + const module = { exports: {} } + if (!script) + Object.assign(box, { module, exports: module.exports }) + vm.runInNewContext(implementation.code, box) + const exported = script ? box.artplayerPluginVttThumbnail || box.window.artplayerPluginVttThumbnail : module.exports + const factory = typeof exported === 'function' ? exported : exported.default + return { + factory, + exported, + art, + box, + styles, + classes, + controls, + listeners, + proxies, + timers, + requests, + progress, + resolve(source = text) { resolveFetch(response(source)) }, + reject(error) { rejectFetch(error) }, + async emit(name, ...args) { return Promise.all((listeners.get(name) || []).map(callback => callback(...args))) }, + async hover(percentage, type = 'hover', event = {}) { + return Promise.all(implementation.profile === 'mouse-source-only' + ? (proxies.get('mousemove') || []).map(callback => callback({ pageX: 20 + progress.clientWidth * percentage })) + : (listeners.get('setBar') || []).map(callback => callback(type, percentage, event))) + }, + } +}