test(thumbnail): [PKG-TOOL-THUMB-05] verify generated sheets across core versions

This commit is contained in:
Harvey Zhao committed 2026-09-16 10:29:20 +08:00
1 parent fb78328c43
commit 8aa99ef03f
13 files changed
+15502 -9

No files matched your search

@@ -134,6 +134,27 @@ The [public type guide](types/README.md)
documents declaration ownership, module entries, events and installed consumers.
See [task plan](../../refactor/plan.md) and [risk ledger](../../refactor/risks.json).
## Generated sheets consumed by ArtPlayer
The tool owns both `videoUrl` and `thumbnailUrl`. Destroy the player borrowing
those URLs before generating a replacement sheet or destroying the tool. Pass
the same `number`, `column`, `width` and `height` to the player's thumbnails
option; the PNG includes a 30px attribution footer, so do not infer cell height
from the full image height. The integration suite uses `scale: 1`.
Run `yarn test:browser:source thumbnail-core.spec.js --workers=1` for native file
extraction, two generated sheets, real mouse hover, screenshot pixels and URL
ownership against associated core 3.5.31, published 5.4.0 and the candidate core.
`ARTPLAYER_THUMBNAIL_ARTIFACT` selects the installed tool main. Historical cores'
first-cell/row-boundary defects have explicit expectations; the candidate must
use the correct cell. The old tool uses its original 300ms delay, while the
candidate still exercises 20ms. Recovered 3.5.31 at 20ms produced black cells in
Firefox because it draws after a timer without awaiting seek completion.
Windows WebKit Blob controls do not validate extraction. See the
[combination checkpoint](../../refactor/changes/2026-09-16-PKG-TOOL-THUMB-05-core-combinations.md)
for exact passing scopes and remaining Safari/device/archive evidence.
## Type and provenance boundaries
All nine executable source modules and the shared type module are checked with
+6
View File
@@ -35,6 +35,12 @@ BASE-01 于 2026-09-10 固定核心与 chapter 的 npm 发布内容,数据在
## 公共 API 报告
Thumbnail 工具的关联历史核心 3.5.31 已单独恢复完整 npm 归档,见
[144 个文件与来源](thumbnail-core.json) 和
[三代核心组合结果](thumbnail-core-validation.json)。运行
`node --test refactor/scripts/thumbnail-core.test.mjs` 校验归档和历史关联。
工具本身的旧 3.5.31 完整归档仍未恢复;不能将核心归档当成工具分发证据。
Audio Track 的后续包基线由 PKG-AUDIO-01 独立冻结,见
[契约和重跑](audio-track-contract.md) 及 [1.1.0 归档来源](audio-track-release.json)。
它不改写本目录初始 BASE 快照,媒体/类型/设备通过范围由后续任务提供。
File diff suppressed because it is too large. Load diff
+347
View File
@@ -0,0 +1,347 @@
{
"schemaVersion": 1,
"task": "PKG-TOOL-THUMB-05",
"sourceCommit": "fb78328c4355042b578091e1236c2ce71a68e5c2",
"capturedAt": "2026-09-16T02:00:14.335Z",
"metadata": {
"url": "https://registry.npmjs.org/artplayer/3.5.31",
"status": 200,
"sha256": "d57dc330fd334660fe892c56b5f552f435cea6f5e86e68a16d0ee32a551ee17e"
},
"association": {
"commit": "af1a116a31b088efdd77afa81c4b6565d12bd162",
"file": "packages/artplayer/package.json",
"version": "3.5.31"
},
"scope": "Original core release associated with recovered tool 3.5.31; not a minimum-version claim or recovery of the missing original tool archive.",
"release": {
"name": "artplayer",
"version": "3.5.31",
"metadataUrl": "https://registry.npmjs.org/artplayer/3.5.31",
"tarball": "https://registry.npmjs.org/artplayer/-/artplayer-3.5.31.tgz",
"integrity": "sha512-xuuU8+NKdurlAxjHzZmesve/WKAz4PRCpwREDsZAyhxKeSknDbVDs1QwWiBrGhRQrQJs/qDEMV5y860imtv2ig==",
"sha256": "6295c423883980f9e2e2e1281c33de09af5b95e2f9caeaf17ba4914a7908dd33",
"manifest": {
"name": "artplayer",
"version": "3.5.31",
"expose": "Artplayer",
"description": "ArtPlayer.js is a modern and full featured HTML5 video player",
"main": "dist/artplayer.js",
"types": "types/artplayer.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/zhw2590582/ArtPlayer.git"
},
"keywords": [
"video",
"player"
],
"author": "Harvey Zack <laozhaochaguan@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/zhw2590582/ArtPlayer/issues"
},
"homepage": "https://github.com/zhw2590582/ArtPlayer#readme",
"dependencies": {
"option-validator": "^2.0.6",
"screenfull": "^5.1.0"
},
"gitHead": "af1a116a31b088efdd77afa81c4b6565d12bd162"
},
"files": {
"package/LICENSE": "e10af0b158a3cd8d923fd524b234cc2ee7219f08746ec3851e71e59d5061f2c2",
"package/README.md": "e59e7d43e0bdc774f9f1c6192d30de5c0d475240dd834c46ca8e68f5e058515b",
"package/dist/artplayer.js": "f6e6c4f5885ce62e17c7955a95db399a734ea715cb7891c20639d325e3da4de3",
"package/package.json": "cba20e72d58ccb2ca435b485223eb66e1a52647f6128f61eccd5526395821b1f",
"package/src/config/index.js": "1141882b3efb492d0a77ccb3777bdedb462b450eaa31fdc1ab09991dd4aff2e5",
"package/src/contextmenu/aspectRatio.js": "9941303fc7b842baab4baf3a1a4f5ba768ee17862ccb5c36b49a41544d940042",
"package/src/contextmenu/close.js": "e1d9cda75b2b2ed469772d77130d78e7cf98fe368d2798dbc0753f842aa3f8f2",
"package/src/contextmenu/index.js": "aa8170c46b2a6ebdd84018af823cce8307c18e252b844f7e60cbeabba7cd8915",
"package/src/contextmenu/info.js": "0f0a7a215d74fb5698467c6dad175b5c58a29710fa148d9205e4a8c35e23b900",
"package/src/contextmenu/light.js": "f65b064948da8d4173aeeaf4ff318c7133913fb22f30c778b5663d4ef4790cda",
"package/src/contextmenu/playbackRate.js": "c3c7d9a87475d130066141be2d3cc5c528791e7aec99d89c853eaed5a6d69a48",
"package/src/contextmenu/version.js": "b1a3f58c3b5e54fb8b4a3a19d884839dc168f4a0d35a6e89779f9fadcb1076e3",
"package/src/control/fullscreen.js": "480b88414c9a68c2410983a5428bd8b34250264ff678249277adcae9966ce85d",
"package/src/control/fullscreenWeb.js": "4e1b40b7a9165ff025f0b3a5197b05a46e135aa54c726cf632d8df53ccdd9e6a",
"package/src/control/index.js": "98cd156158337d8f638fa53a19bf43d65ff1796b06fe14204b2a654e507c743a",
"package/src/control/loop.js": "074804c86398b8706d70392f38aec5a5c9cf9dbef116ef4660533caa36b5f8da",
"package/src/control/pip.js": "9bca1805ac5e722207f2d4c2ea8eb066b23acaa300c50261bd64bcf19aaf4cd2",
"package/src/control/playAndPause.js": "1e737a76786146e7e16fec704651a2e80284cc796154b100f6cf342ac0474d62",
"package/src/control/progress.js": "869294624065ba755e97d8dbfa918a2d7b14df6bc21182ebf910a0d0cc9b99cd",
"package/src/control/quality.js": "7b83a706f922ca6332bb00be8a84f3f74f970f39b88ed308f99a4384f81e1da8",
"package/src/control/screenshot.js": "56358d7c49b98f43b463b7150bb917b3828089fb3461dc596b4462bd56b90a4d",
"package/src/control/setting.js": "57edc60a847cda957709ce077309530bdb7580c4139cac648be88a44d8697fee",
"package/src/control/subtitle.js": "296f582aa364e36f6980102a52bb05f72cffd990dc3cc3bbc25142ec76d84235",
"package/src/control/thumbnails.js": "941d9e620d90f460b97a2fad16c018e1160ac04c61caf9f4ec13b897e8e7012e",
"package/src/control/time.js": "1f0b7c37c0f9f5f6f62826b7fcf4e6c1b403a532cfbeed4491ddb8cd0cf93d8d",
"package/src/control/volume.js": "1f74dc0a9c46596d9bb67e5c5b0826724d757b608b205d64176f3d2df266fad0",
"package/src/events/clickInit.js": "2d1677389199b167c669b6eaa1ba9a1b322c25a0118b4f80122ff96097222225",
"package/src/events/gestureInit.js": "59b69dd52d7adb8714e310f1b4fdab31f6c8ff7cc1b51654353c1fcdd638f2fa",
"package/src/events/hoverInit.js": "efc52a3efa115bb9bf40eb98f9b6f5eb62bcc5f1a6f536b8d31e21070d05f2e9",
"package/src/events/index.js": "1de72910e10231955226030ceeaf83612c5b8784cc7dd170c95eb1f1eefcdc36",
"package/src/events/mousemoveInit.js": "99383ba26bfb723f90fd0b068de4042c874bc630f20881b2ef0986bb0d353dd2",
"package/src/events/resizeInit.js": "3e1a968b609bfdd6900864b7addedc06aaaa0b3938b49db2fd976eaf95eb67f4",
"package/src/events/viewInit.js": "f0b6b13dec7cb6589ac7a5292ccb0adfbc86233c3e5a2f6ed632e2d8b269863a",
"package/src/hotkey.js": "0ff31f8398dae512d9cab66cb6c3a81b17ad82ca79027a0d404d8ea6a960faad",
"package/src/i18n/index.js": "10b3af2a0c676cd216ef4e3354b301336d3579cb91a2e5344c0d19497ea4a6e1",
"package/src/i18n/zh-cn.json": "87e759a3d555abf9d6a7c980dedf965b2557d715ca0e67f8b6928d4ffc46d5a0",
"package/src/i18n/zh-tw.json": "a76993ab9077a848ca0cebfb9e94ff18b3301aacd7f2cd9d453f2900345eeaaa",
"package/src/icons/fullscreen-web.svg": "82d3ebc7ac89283a3dcdaea606e518ca0775df71c062d3e07bfc8e4e11bce93b",
"package/src/icons/fullscreen.svg": "26e83a9ac001d0710d064963a56a34afde309503873c3bc9d588b8807ec8015c",
"package/src/icons/index.js": "26ddf832d241e4dbbb525b41b37af3e8da9e2d51c0a5404328916566673f69cb",
"package/src/icons/loading.svg": "ffcf5d814e6f3931a370bb4f0d2675381c2adb8f6d29dd1ee8f5df33d8175e75",
"package/src/icons/next.svg": "9967c458a78032a3be130cd4ea59b70b8324878468cade18361d50567a8c9618",
"package/src/icons/pause.svg": "9efe05ebf5610befddebcac51b3c9a491001189ff86204cb47928eb1617f6c7a",
"package/src/icons/pip.svg": "52d1e150d1364aed20917d83a2f7ab7589fb9df9655f3d3afaea3c4387800812",
"package/src/icons/play.svg": "c35562d20028d52ce329f35f90496a06d4dc5b6dc39df7ebcfb027f6f2393958",
"package/src/icons/prev.svg": "5be9100d186dfd629e88afa25b0f5fd120ce90d47efcc970a00436c458319692",
"package/src/icons/screenshot.svg": "49f7d90198fb0bcbc29848f40b0a8c042b78c07ecef1cfc5184e2e84a61b4bd4",
"package/src/icons/setting.svg": "7f6454d1c7f037d5a7144a898abedc89ca339534166d620dfa45efcd4c74ee3c",
"package/src/icons/state.svg": "271a80268957209b2455ccccf4a42e89fafe990d04b2bcbe22624fcd63e43411",
"package/src/icons/subtitle.svg": "b28a64d59b2eca9bde9357b50ff6701452dffe62e8c568c514b2e36542f83f9c",
"package/src/icons/volume-close.svg": "b9dbeaf9043c8014cd29d0e4a21a82ce567bc6b85497c310cca5f68bceeb31b2",
"package/src/icons/volume.svg": "baebc4ede3013202a4b13202a8d961cb0e5e065ee03733816022b2541d13884f",
"package/src/index.js": "0099495a167656c3af9408d4559cbdb2d573f480a0807698bc5c22f316be5ade",
"package/src/info.js": "0ac75176efb8c1626bd0f08831a009b4b7b478582aff4f06247512b85f410f22",
"package/src/layer.js": "d801f40fd34425fbe347cfec353c40a0286e569590b3c9beb262936eb512f266",
"package/src/loading.js": "f47a310ae4acfbd45bf34786901377844a08b4acc40d169f99b1af7ab02e4988",
"package/src/mask.js": "945e8d45b9f1a683d47e2981fce45441da3fe399d09de0829781a486d9ee513a",
"package/src/mobile.js": "362b4a3be51eeed87c5953a5a70df250d12144a1359b1f639de39e0409088e6d",
"package/src/notice.js": "84a8fbb2035c8c12b80ae43a2fe4abb76152c936acdec9a3ff6a6d918bd53c29",
"package/src/player/aspectRatioMix.js": "d6de0761fdc46e5235dcee296859549174819d24528912940052f266536fc89f",
"package/src/player/attrInit.js": "3802cfb908fd333f6d7a34be2fe5f19760271a953a54f7eceb9d32caf5e93e8a",
"package/src/player/autoSizeMix.js": "1337da3bd01d317690c2e9ea0b9e582818c0b2a6b02f475001bfe9bd09049b91",
"package/src/player/currentTimeMix.js": "8b23cf52262284e1a974007a2d61bf7c30cb4bdb25405e1380d47649e86f4d1e",
"package/src/player/durationMix.js": "3a8efa85852b53e1a22231a858bc05e32e7a08c138d16fd6a8b279fa67d74183",
"package/src/player/eventInit.js": "08a551bb724f3886bdb33450e280c5964ce7521dc5247fcf068eeadb7b95a3a5",
"package/src/player/exclusiveInit.js": "f21f07d70779192039160cd5e1e404263bca4080f97a23ded8893bce937e6e7f",
"package/src/player/flipMix.js": "b7d4e456695a6114536b77e21a2b8c9ea61129a8b9c62a24a8fbe3c764c27a54",
"package/src/player/fullscreenMix.js": "f62d862ab451e4e1c9f82841f8bb88f989840bbd2b51e776c1d8e34578196f9d",
"package/src/player/fullscreenRotateMix.js": "ac7060b12d0d828681776348d5cfa48cf3256eee0473c392ff96bd50941d03b8",
"package/src/player/fullscreenWebMix.js": "3cad9355a0e6a09e470f30fb2de34131fbbb4ad815a12bc6d3dfeea897877b96",
"package/src/player/index.js": "14ff2741482c6a459bb24ed0372005100403736a0800c9aae9176294fad7c28b",
"package/src/player/lightMix.js": "4f49ecdec4bc95451721b7df40b3eb95540d26297bb91868a91d09713ee33ec4",
"package/src/player/loadedMix.js": "772169f97e31be3ad1d45c29056a68c6de3f172921d19bc0f430fa2edb188475",
"package/src/player/loopMin.js": "61f89a6cee6ce773e6a2319ac99e01c264781d2f6720f9b9cfb74bcfe66754c5",
"package/src/player/miniMix.js": "96c3e297641b46e2ebfd6e6ad01369dfcf48c2566f43c4587c4006f2694ef313",
"package/src/player/pauseMix.js": "7abbfd904d55a322e76dfebabb43dabe2fd91b1ef32840d0cd18b64f4e229fb2",
"package/src/player/pipMix.js": "32a22db1c33dcd096d067cdf939ed3c70f393441b18f7c8fe53e9a64dbd9675b",
"package/src/player/playMix.js": "b5a413eac658912f5a10311c3d2a656829f5d8c28a14a172955d44b2b16ca0be",
"package/src/player/playbackRateMix.js": "8b6150375f636ea5917fbb311807ab705882f52df9a24dd9e3f99925df6901fc",
"package/src/player/playedMix.js": "5516d2fecfd048320544cda28ebe368459816bf2201eaf6be7b756630afee3a6",
"package/src/player/playingMix.js": "dc9bc49920c20e7504f8adab7b4c50ffa9d2f65ad078d62c492a19505abb192d",
"package/src/player/posterMix.js": "6551399ec68e90ec0dca5d154df234aae550eacbe8cc6e8b13555ef719db02c0",
"package/src/player/rectMix.js": "46354ef4ed7fa0c737bf5de9c96825491a78b65932d5a488798ae0cb9e3a88e1",
"package/src/player/rotateMix.js": "e96eb474484aa45472a6c1c97d877a46d2414b831568e7b76c1d6de60299d5c8",
"package/src/player/screenshotMix.js": "a30ba49d23817ad7cbc0b76189ccaf168e2c253058574a850acdb948659f29c0",
"package/src/player/seekMix.js": "1be19f1ee0a502327d158ed2b35da8c5a78750bf779558d058b565fc0643aa7b",
"package/src/player/switchMix.js": "a2b78e2ab7b100bd36af27408ae59255c58e271ea065aef2c48e98b0c24b7203",
"package/src/player/toggleMix.js": "af06c80fd60a89eb453c4d9dc54416f8782a840e9f30f8dafac203ce0cd2e7cc",
"package/src/player/urlMix.js": "c79c779438d442cf4fe6ab6f73229fa7cebc1bfdbee4e434a54303db658a9bf6",
"package/src/player/volumeMix.js": "676f4c05459f8cd6e788d24f16614e48a34ae0def957832c925da7c53744ba77",
"package/src/plugins/index.js": "4a929c4bd20141dc7c96674aa29e38217fbab2ae8decbc65fdcdda27f3aa3338",
"package/src/plugins/localSubtitle.js": "faa70fe2aa7d46e62d991d8cefb09ac4f6b361919f4a5ef5b68ef03737ec1c60",
"package/src/plugins/localVideo.js": "629062e7d2018fc899265b5f84921d7d2168cee1b8eda04ca589e86ed1bb3dad",
"package/src/plugins/log.js": "c1effb811e1112763c7c99f99d5f489078ced11ab898a0a019a41c6cde739edd",
"package/src/plugins/miniProgressBar.js": "98be20eba83482a1fb7469f4228b5916a9f6b5cae88f73de461cea453b2419e7",
"package/src/plugins/networkMonitor.js": "4ef263c21b29e47098fab7bf98374036578daefb5854855190077a088af9e65d",
"package/src/plugins/subtitleOffset.js": "f76806d0c21698f5b44d7a9d0ae89083cbb15888f6b462d199d22d7b4850e837",
"package/src/scheme/index.js": "93127f49ccf4f1d6643594be2368a2d7529ad6498e7518fd49c88b63c2189aab",
"package/src/setting/aspectRatio.js": "68783962408a11adb8fa01cf0dabeb68374438fa7e9a116024714a4999691eda",
"package/src/setting/flip.js": "e541d8c4990875bf37dc06e240da887c2869286216b319e7ffcff493f1a56378",
"package/src/setting/index.js": "1cb2d855bc6da6212bee11c4827e98a57203b7d22d21d25ab6e38dd60aa1729f",
"package/src/setting/playbackRate.js": "54ccbac34711894c28ac48d39497c8ff22d30aa3a83ef90485dff0cc71d08c44",
"package/src/setting/rotate.js": "a38dfdbae6cf6f3c8a83f7b55bb5d03fbe4422e805a79849d8ceb9232dc8e622",
"package/src/storage.js": "6ce11595322cef50346c95c511d3968ed31bd0a90af64d83f13e12f841881c5c",
"package/src/style/artplayer.scss": "37676394ef97246b9fcb62bd0188efd37cc79ec50585032caf29f7af9c5bdd3a",
"package/src/style/aspectRatio.scss": "c2e5a5c0d4f4a740f8bbe30cbbdc71650ec0790e05bdba3929ef5d08a86ea9c6",
"package/src/style/autoSize.scss": "35f482ca46bf9d12e8a1430b18502d7466468539bf642f686a46650f86fa7817",
"package/src/style/balloon.scss": "c8186155a7dd804b3df9490b19cd426f1d8429cb67d11551e0c27bbc2724a1ff",
"package/src/style/bottom.scss": "dd69ddb97cf0347f03500b4fdb1381cf065fb0011436e2227988be91ae8c92d1",
"package/src/style/contextmenu.scss": "eb1e488e46c43f6188e0a3d4914b8faeba6fb777f8265501078e77d516d3572c",
"package/src/style/danmuku.scss": "f2f488c5140ee5cc09226784e1222fcd0b9b917f7e86179ff9922abf4a90edd8",
"package/src/style/flip.scss": "d18c9c258041d7dc2128f709cacfbcf3a93cd498deed0a733ed90631a352b148",
"package/src/style/fullscreenRotate.scss": "9be8d1340d4d25cdf76eab1eab550ead89cbc655628e0d4cc9231d98de73ff86",
"package/src/style/fullscreenWeb.scss": "cc1a35fb81507faae404d08ed3bd52cc4fc15243937c992c23a3ab443719bb31",
"package/src/style/hideCursor.scss": "e05a85e4c0afd202a0becc2f0f5fb4bb1b49f56802c495f2a816099416d9c22b",
"package/src/style/index.scss": "02eeddf75b25f3635aba0851d15386ceee7398dfc54c0596149520db6b32f016",
"package/src/style/info.scss": "d40b6a710550d3cb46e5fda9fc3fad8aca617a56872a5c1d8ab59154fa185806",
"package/src/style/layers.scss": "db19d955e243da2a888204115001d6f1567d7203427d9975613fb9a5d617802d",
"package/src/style/loading.scss": "fd1411beb6390c71ba4f7a812e32c8e28c2fb7b3dac54a02d20f3278cd2892f8",
"package/src/style/mask.scss": "7ebc125d3dcf69fd0e6635502028447334d04a395fcc50a86e0ba9307a36703a",
"package/src/style/mini.scss": "2d1d35a24f2bebb801f9c13f18f141286fa5fc9908ddfce5a16f65eee18c74f1",
"package/src/style/notice.scss": "2793a0f603c049df0ca5cec62682ab6136b249a31a53fd8caa04dd250409e4dc",
"package/src/style/plugins.scss": "4fee8cc639e3efd8e9910a9d15a4ee80e5adeffae38aac52a5474ef02c0b92c0",
"package/src/style/poster.scss": "3039b30d18bcae180f51a166f01f8a331d417bea4df900d44fec3006167ddc65",
"package/src/style/selector.scss": "d834f5e0450331096c33eb055b10bfcf7aeea3bf51b9df5ccdb33b5077274569",
"package/src/style/setting.scss": "0c05f8c5a7d895d6521fb73c36931121f5892c510e06b352f247206643fd4740",
"package/src/style/subtitle.scss": "2372ca5120da184d487e5f1c4d3a8a5eb5a5be1c7172a76fe11c10c9d0a417e6",
"package/src/style/video.scss": "c8a4fcdf7070a70b724e6d39d78b9c243ff18d8f8b6fbb8700d647799ff9756b",
"package/src/subtitle.js": "2b09bd8b2e5d8bb4ff6736470de9cc0d7f0817bc317860722b5aeb2a39082e9b",
"package/src/template.js": "f76586b89c11e9435ff1d457d9af4351d81397160ae8251d90adadd24179824c",
"package/src/utils/compatibility.js": "e0f6f697d258867d997a02743281037a2b543190fdd3a2f15b2a771073fe4f20",
"package/src/utils/component.js": "705b656bf8029c059b74d29df8d1f5748f3bfc9d7bc05a4c9368d10490a3f06e",
"package/src/utils/dom.js": "de97d2b717b763f9cddcc5f4d6886e9f53ded5343a58f6f7f9318a73ef745fac",
"package/src/utils/emitter.js": "637c2b65c89a34ca3208335a892258540eefc49cb8f87f981c69496fe49efa89",
"package/src/utils/error.js": "3b395e07922fb3fcbb7de34e662ada1914b3499dac6f109e522c432bd705f3d1",
"package/src/utils/file.js": "c02139c54e569bf635f90d32b7ccb9d6cb8ba5ff126071e6c79b34a134b11cb2",
"package/src/utils/format.js": "64e22ffdd2bc27808847e118db413c4274bb8c33da21071fb535f0751848db9d",
"package/src/utils/index.js": "585394c7bfda8e8548cfb92c73ae1e45cccc55c48d683ca9a48099debdf4b98e",
"package/src/utils/property.js": "a78e476a111010d0760ca64e95d692736ca91c245a1eefd004603ed51b740f0a",
"package/src/utils/subtitle.js": "7d4f67bedab5c32ef4c4b8c2b8d61c2d25d76c79acd236f88d7113b314d7c9d3",
"package/src/utils/time.js": "905f0abb54088ea9320f00e7668efe65a398efa3f1a66d5f79f9d15eacebc2d6",
"package/src/whitelist.js": "04dbac182814dc575707682bc48f723f4a0746f76ae185694837f3b4dc273bc6",
"package/tsconfig.json": "2a93a271339a5b743f4b42090b9a633b79ba3657a5e429e00d9e3f5dd1ce6e30",
"package/types/artplayer.d.ts": "3782fa31d484433976f464e44e15d2dce1be6c2b4cb45cc727d0e2b92e53f4d6"
}
},
"archiveMembers": [
"package/LICENSE",
"package/README.md",
"package/dist/artplayer.js",
"package/package.json",
"package/src/config/index.js",
"package/src/contextmenu/aspectRatio.js",
"package/src/contextmenu/close.js",
"package/src/contextmenu/index.js",
"package/src/contextmenu/info.js",
"package/src/contextmenu/light.js",
"package/src/contextmenu/playbackRate.js",
"package/src/contextmenu/version.js",
"package/src/control/fullscreen.js",
"package/src/control/fullscreenWeb.js",
"package/src/control/index.js",
"package/src/control/loop.js",
"package/src/control/pip.js",
"package/src/control/playAndPause.js",
"package/src/control/progress.js",
"package/src/control/quality.js",
"package/src/control/screenshot.js",
"package/src/control/setting.js",
"package/src/control/subtitle.js",
"package/src/control/thumbnails.js",
"package/src/control/time.js",
"package/src/control/volume.js",
"package/src/events/clickInit.js",
"package/src/events/gestureInit.js",
"package/src/events/hoverInit.js",
"package/src/events/index.js",
"package/src/events/mousemoveInit.js",
"package/src/events/resizeInit.js",
"package/src/events/viewInit.js",
"package/src/hotkey.js",
"package/src/i18n/index.js",
"package/src/i18n/zh-cn.json",
"package/src/i18n/zh-tw.json",
"package/src/icons/fullscreen-web.svg",
"package/src/icons/fullscreen.svg",
"package/src/icons/index.js",
"package/src/icons/loading.svg",
"package/src/icons/next.svg",
"package/src/icons/pause.svg",
"package/src/icons/pip.svg",
"package/src/icons/play.svg",
"package/src/icons/prev.svg",
"package/src/icons/screenshot.svg",
"package/src/icons/setting.svg",
"package/src/icons/state.svg",
"package/src/icons/subtitle.svg",
"package/src/icons/volume-close.svg",
"package/src/icons/volume.svg",
"package/src/index.js",
"package/src/info.js",
"package/src/layer.js",
"package/src/loading.js",
"package/src/mask.js",
"package/src/mobile.js",
"package/src/notice.js",
"package/src/player/aspectRatioMix.js",
"package/src/player/attrInit.js",
"package/src/player/autoSizeMix.js",
"package/src/player/currentTimeMix.js",
"package/src/player/durationMix.js",
"package/src/player/eventInit.js",
"package/src/player/exclusiveInit.js",
"package/src/player/flipMix.js",
"package/src/player/fullscreenMix.js",
"package/src/player/fullscreenRotateMix.js",
"package/src/player/fullscreenWebMix.js",
"package/src/player/index.js",
"package/src/player/lightMix.js",
"package/src/player/loadedMix.js",
"package/src/player/loopMin.js",
"package/src/player/miniMix.js",
"package/src/player/pauseMix.js",
"package/src/player/pipMix.js",
"package/src/player/playMix.js",
"package/src/player/playbackRateMix.js",
"package/src/player/playedMix.js",
"package/src/player/playingMix.js",
"package/src/player/posterMix.js",
"package/src/player/rectMix.js",
"package/src/player/rotateMix.js",
"package/src/player/screenshotMix.js",
"package/src/player/seekMix.js",
"package/src/player/switchMix.js",
"package/src/player/toggleMix.js",
"package/src/player/urlMix.js",
"package/src/player/volumeMix.js",
"package/src/plugins/index.js",
"package/src/plugins/localSubtitle.js",
"package/src/plugins/localVideo.js",
"package/src/plugins/log.js",
"package/src/plugins/miniProgressBar.js",
"package/src/plugins/networkMonitor.js",
"package/src/plugins/subtitleOffset.js",
"package/src/scheme/index.js",
"package/src/setting/aspectRatio.js",
"package/src/setting/flip.js",
"package/src/setting/index.js",
"package/src/setting/playbackRate.js",
"package/src/setting/rotate.js",
"package/src/storage.js",
"package/src/style/artplayer.scss",
"package/src/style/aspectRatio.scss",
"package/src/style/autoSize.scss",
"package/src/style/balloon.scss",
"package/src/style/bottom.scss",
"package/src/style/contextmenu.scss",
"package/src/style/danmuku.scss",
"package/src/style/flip.scss",
"package/src/style/fullscreenRotate.scss",
"package/src/style/fullscreenWeb.scss",
"package/src/style/hideCursor.scss",
"package/src/style/index.scss",
"package/src/style/info.scss",
"package/src/style/layers.scss",
"package/src/style/loading.scss",
"package/src/style/mask.scss",
"package/src/style/mini.scss",
"package/src/style/notice.scss",
"package/src/style/plugins.scss",
"package/src/style/poster.scss",
"package/src/style/selector.scss",
"package/src/style/setting.scss",
"package/src/style/subtitle.scss",
"package/src/style/video.scss",
"package/src/subtitle.js",
"package/src/template.js",
"package/src/utils/compatibility.js",
"package/src/utils/component.js",
"package/src/utils/dom.js",
"package/src/utils/emitter.js",
"package/src/utils/error.js",
"package/src/utils/file.js",
"package/src/utils/format.js",
"package/src/utils/index.js",
"package/src/utils/property.js",
"package/src/utils/subtitle.js",
"package/src/utils/time.js",
"package/src/whitelist.js",
"package/tsconfig.json",
"package/types/artplayer.d.ts"
]
}
@@ -0,0 +1,73 @@
# PKG-TOOL-THUMB-05 三代核心组合验证检查点
起点 `fb78328c4355042b578091e1236c2ce71a68e5c2`。本批补齐真实文件抽帧后由
新旧核心显示图片的证据,没有改变生产实现、公开类型或依赖。05 保持 doing:
Windows WebKit 的 Blob 解码限制与实际 Safari/设备证据仍未关闭。
## 实现和兼容
冻结关联核心 artplayer 3.5.31 的完整 npm 归档,逐个验证 144 个成员 SHA256、
registry integrity、归档 SHA256 和 manifest。历史 Git 核心版本关联一致,见
[归档](../baselines/thumbnail-core.json)。这是历史关联版本,不是最低支持版本,
也不代表找回了原始 thumbnail 工具的完整归档或声明。浏览器服务新增独立路由,
保留原 published 5.4.0 路由,每次读取先校验归档和 main。
`thumbnail-core.spec.js` 将恢复的 CDN 3.5.31、冻结工作区工具和重构工具的
默认/workspace 策略分别接入核心 3.5.31、5.4.0 和重构核心。原生文件选择、
实际解码、工具生成 PNG、真实鼠标悬停,每组两次生成并重建播放器,检查图片
640×300、12 个样本中心、5 个取格位置、预览像素、URL 更新和销毁。
截图比较上部未被时间提示遮挡的区域,排除圆角,平均 RGB 通道误差上限为 3;
没有隐藏提示或修改产品样式。
旧核心在行边界取上一行最后一格、返回首格时保留上次位置,是已有缺陷;旧结果
明确断言,重构核心必须显示正确格子。核心 5.4.0 初始化会撤销空的前一媒体 URL,
两次初始化精确记录两个空值;所有非空创建/撤销仍精确匹配。
图片有 30px 页脚,应显式传入与生成配置一致的 number/column/width/height;
本组合 scale=1。工具持有图片和媒体 Blob URL,先销毁引用它们的播放器,再生成
下一张或销毁工具;播放器不得撤销外借 URL。包内架构和测试文档同步补充。
## 验证
Node 24.21.0 / Yarn Classic 1.22.22 / Windows。逐例记录和哈希见
[组合证据](../baselines/thumbnail-core-validation.json)。
| 范围 | 结果 | 实际证明 |
| --- | --- | --- |
| 关联核心归档测试 | 1 通过 | 144 个文件、完整归档和历史版本关联 |
| 源码三引擎组合 | 36 通过,144.262s | Chromium/Firefox 24 个抽帧组合;WebKit 12 个能力控制 |
| 已安装 main 三引擎组合 | 18 通过,53.185s | Chromium/Firefox 12 个抽帧组合;WebKit 6 个能力控制 |
| 最终通知等待改动 | 源码 WebKit 12 通过 | 等待精确一次原生错误通知 |
| 只读 scoped ESLint | 通过 | 浏览器用例、服务、归档测试 |
| 计划与风险台账 | 通过;风险回归 2 项通过 | 285 项任务、依赖、证据链接和风险关闭规则 |
以上最终运行没有失败/跳过/重试。安装工具来自先前独立 Yarn pack/install,
main 与当前 dist SHA256 一致:
`a254d9d66943bd302719edae040d6ee5e0a32f7c9f1ae0727be8cb6c5a335511`。
组合中的重构核心来自源码编译,不冒充全套安装产物或最新全项目候选验收。
共享测试输入变化后,REL-02 仍须刷新完整候选指纹。
Windows WebKit 26.6 原生 HTTP MP4 成功,同一内容 Blob URL 报 media error 4。
控制用例确认原生和工具一致、通知与清理正确,没有产生缩略图,不能计作 Safari
抽帧成功。Chromium 153.0.8010.12 / Firefox 155.0 验证实际图片和预览。
## 失败调查和修正
- 初次将另一素材的末格红色预期错误套用到本素材,12 项失败。独立 FFmpeg 在
1、3、…、23 秒解码确认所有中心蓝色,修正预期并保存原报告。
- 随后 12 项整图比较失败,截图确认时间提示遮挡下部。改比较未遮挡区域,
没有扩大误差阈值。一次已发布核心空 URL 撤销差异改为按实际代码精确断言。
- 统一 delay=20 时,Firefox 三个恢复版 3.5.31 组合生成黑格:0/4/8 格中心蓝色,
其余黑色。历史源码只等计时器后 drawImage,不等 seeked。正常组合使用旧版原
默认 delay=300 后通过;重构工具仍使用 delay=20。保留失败,不声称旧版短延时通过。
- 安装产物首次 17 通过/1 失败:video.error 已设置,排队的 error 事件尚未送达。
改为在原超时内等待精确一次通知;安装 18 项和源码 WebKit 12 项通过。源码完整
36 项在这次等待改动之前运行,其测试文件另存哈希;跟进结果单独记录。
- 归档测试纠正错误的 Vitest 导入,使用项目原有 Node 运行器,没有新增依赖。
## 交接与回退
225 done /23 doing /37 todo。THUMB-LIFE-01 增加组合证据但保持开放;原生 Blob
风险仍保留。05/06 的 Safari/真机和完整历史工具分发证据不足,不结束任务,
不进入用户保留的正式复盘,不推送或发布。回退仅撤销本批测试路由、归档/证据、
用例和文档,不改变生产 API。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 285 项,范围 22 个包及工作区/示例。
状态:todo 38 / doing 22 / blocked 0 / done 225 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 37 / doing 23 / blocked 0 / done 225 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -400,7 +400,7 @@
| PKG-TOOL-THUMB-02 | artplayer-tool-thumbnail<br>建立特有行为与错误测试 | PKG-TOOL-THUMB-01, ENG-03, ENG-05 | 文件输入/拖放、抽帧网格、begin/end、进度/失败/重复任务 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-TOOL-THUMB-03 | artplayer-tool-thumbnail<br>整理内部职责与资源 | PKG-TOOL-THUMB-02, CORE-01 | 输入、抽帧队列、网格导出、URL/监听清理分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-TOOL-THUMB-04 | artplayer-tool-thumbnail<br>迁移自有源码和公开类型 | PKG-TOOL-THUMB-03, ENG-04, ENG-06, CORE-07, PKG-TOOL-THUMB-07 | 补齐真实 API 声明,自有 emitter/utils TS 化 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;为 BASE-DIST-01 提供兼容入口/声明及消费者验证 | H | done |
| PKG-TOOL-THUMB-05 | artplayer-tool-thumbnail<br>验证新旧核心和组合 | PKG-TOOL-THUMB-04, CORE-22 | 工具独立浏览器使用,生成缩略图在核心中显示 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-TOOL-THUMB-05 | artplayer-tool-thumbnail<br>验证新旧核心和组合 | PKG-TOOL-THUMB-04, CORE-22 | 工具独立浏览器使用,生成缩略图在核心中显示 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing |
| PKG-TOOL-THUMB-06 | artplayer-tool-thumbnail<br>验证分发并同步文档 | PKG-TOOL-THUMB-05, ENG-07 | tool.thumbnail.js、历史 ESM 兼容文件、types 路径和 tarball | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
| PKG-TOOL-THUMB-07 | artplayer-tool-thumbnail<br>恢复清理和销毁中的原始异常传播 | PKG-TOOL-THUMB-03 | 清理记录与异常值分离,保留首个 falsy 抛出值并继续释放资源;历史对照、真实DOM、分发验证及维护记录 | 恢复旧 destroy 监听器同步异常语义,0/false/undefined/NaN等不被吞掉或覆盖;后续清理、重复销毁与正常抽帧回归通过,默认兼容决策不受影响 | M | done |
@@ -677,6 +677,7 @@
- PKG-TOOL-THUMB-02: [记录](baselines/thumbnail-behavior-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md)
- PKG-TOOL-THUMB-03: [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md) [记录](baselines/thumbnail-input-checkpoint.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md) [记录](baselines/thumbnail-lifecycle-validation.json)
- PKG-TOOL-THUMB-04: [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md) [记录](baselines/thumbnail-runtime-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md) [记录](baselines/thumbnail-public-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md) [记录](baselines/thumbnail-emitter-validation.json) [记录](thumbnail-compatibility-decision.md) [记录](changes/2026-09-15-PKG-TOOL-THUMB-04-policy-proposal.md) [记录](changes/2026-09-15-PKG-TOOL-THUMB-04-approved-policy.md) [记录](baselines/thumbnail-policy-validation.json)
- PKG-TOOL-THUMB-05: [记录](changes/2026-09-16-PKG-TOOL-THUMB-05-core-combinations.md) [记录](baselines/thumbnail-core-validation.json) [记录](baselines/thumbnail-core.json)
- SITE-01: [记录](site-inventory.md) [记录](baselines/site-inventory.json) [记录](baselines/site-provenance.json) [记录](baselines/demo-additions.json) [记录](changes/2026-09-14-SITE-01-site-inventory.md) [记录](baselines/site-inventory-validation.json)
- SITE-SMOKE-01: [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md) [记录](baselines/docs-smoke-validation.json) [记录](scripts/docs-smoke.test.mjs)
- SITE-02: [记录](changes/2026-09-14-SITE-02-editor-declarations.md) [记录](baselines/editor-declarations-validation.json) [记录](changes/2026-09-14-SITE-SMOKE-01-documentation-smoke.md)
+10
View File
@@ -1,5 +1,15 @@
# 进度与证据
## PKG-TOOL-THUMB-05 三代核心组合检查点
新增关联核心 3.5.31 完整归档校验和真实文件抽帧/核心预览组合,见
[记录](changes/2026-09-16-PKG-TOOL-THUMB-05-core-combinations.md)。源码36项、
已安装main18项通过,其中分别24/12项实际抽帧,12/6项为Windows WebKit能力控制。
错误通知等待修正后源码WebKit12项跟进通过。旧工具短延时黑格和旧核心取格缺陷
明确记录;重构工具在两种策略、三代核心下通过实际截图与URL释放检查。
05保持doing,Safari/设备及完整历史工具分发缺口保留。225 done /23 doing /37 todo。
共享输入对应候选仍待REL-02刷新;不进入用户保留的正式复盘,不推送或发布。
## SITE-05 本地链接、编辑器与远端缺口
新增check:site-links并接入CI:67生成页面/5766本地引用/942搜索结果全部有效。
+13 -4
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"task": "BASE-07",
"sourceCommit": "e01f39ff",
"updatedAt": "2026-09-15T17:18:32.442Z",
"updatedAt": "2026-09-16T02:27:53.896Z",
"note": "Open means closure evidence is outstanding; source-observed and unverified are not reproduced runtime bugs. A source issue already mitigated in the workspace still requires candidate regression evidence. No accepted-with-scope item is implied by baseline task completion.",
"items": [
{
@@ -4182,8 +4182,13 @@
"refactor/baselines/thumbnail-input-checkpoint.json",
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md",
"test/thumbnail-lifecycle.test.js",
"refactor/baselines/thumbnail-lifecycle-validation.json"
]
"refactor/baselines/thumbnail-lifecycle-validation.json",
"refactor/changes/2026-09-16-PKG-TOOL-THUMB-05-core-combinations.md",
"refactor/baselines/thumbnail-core-validation.json",
"refactor/baselines/thumbnail-core.json",
"test/browser/thumbnail-core.spec.js"
],
"workspaceState": "04 strict types and approved default/workspace policy are complete. 05 source and installed tool main pass actual Chromium/Firefox extraction, two generated sheets, preview pixels and exact URL ownership against core 3.5.31/5.4.0/candidate. Windows WebKit native Blob error 4 and physical Safari/device evidence remain open."
},
{
"id": "THUMB-PROVENANCE-01",
@@ -4225,7 +4230,11 @@
"refactor/baselines/thumbnail-behavior-validation.json",
"refactor/changes/2026-09-13-PKG-TOOL-THUMB-02-behavior.md",
"test/thumbnail.test.js",
"test/browser/thumbnail-tool.spec.js"
"test/browser/thumbnail-tool.spec.js",
"refactor/changes/2026-09-16-PKG-TOOL-THUMB-05-core-combinations.md",
"refactor/baselines/thumbnail-core-validation.json",
"refactor/baselines/thumbnail-core.json",
"test/browser/thumbnail-core.spec.js"
],
"compatibleResolution": "Preserve independent HTTP/Blob capability controls; obtain actual supported WebKit/Safari file extraction evidence before release acceptance. Do not count the 12 unavailable controls as successful thumbnail generation or silently replace local files with HTTP media.",
"closureCriteria": "Actual generated PNG/extraction and lifecycle proof in supported Safari/WebKit environments, or explicit supported-environment decision with migration documentation; Windows limitation stays identifiable."
+19
View File
@@ -0,0 +1,19 @@
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 { archiveFiles, ensureArchive, hash, readMember } from './releases.mjs'
test('Thumbnail associated core 3.5.31 is a complete verified npm archive', async () => {
const baseline = JSON.parse(fs.readFileSync(new URL('../baselines/thumbnail-core.json', import.meta.url)))
const tool = JSON.parse(fs.readFileSync(new URL('../baselines/thumbnail-release.json', import.meta.url)))
assert.deepEqual(baseline.association, tool.coreAssociations.historical)
assert.equal(baseline.release.name, 'artplayer')
assert.equal(baseline.release.version, baseline.association.version)
const archive = await ensureArchive(baseline.release)
assert.deepEqual(archiveFiles(archive), baseline.archiveMembers)
assert.deepEqual(Object.keys(baseline.release.files).sort(), baseline.archiveMembers)
for (const [member, expected] of Object.entries(baseline.release.files))
assert.equal(hash(readMember(archive, member)), expected, member)
assert.deepEqual(JSON.parse(readMember(archive, 'package/package.json')), baseline.release.manifest)
})
+6 -2
View File
@@ -4274,11 +4274,15 @@
"PKG-TOOL-THUMB-04",
"CORE-22"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "工具独立浏览器使用,生成缩略图在核心中显示",
"acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成",
"evidence": []
"evidence": [
"changes/2026-09-16-PKG-TOOL-THUMB-05-core-combinations.md",
"baselines/thumbnail-core-validation.json",
"baselines/thumbnail-core.json"
]
},
{
"id": "PKG-TOOL-THUMB-06",
+12
View File
@@ -535,6 +535,18 @@ The first immediate-pixel WebKit failure remains in the refactor evidence.
This is native desktop media playback, not physical audibility or full devices.
`document-site.spec.js` also checks generated guide links and Run Code parameters.
## Standalone thumbnail sheets
The standalone tool's `thumbnail-core.spec.js` generates actual PNG sheets from
a selected local MP4 and displays them in core 3.5.31, 5.4.0 and the candidate.
It checks two generations, real hover, unobscured screenshot pixels and exact
Blob URL ownership. Historical grid defects are asserted separately from the
correct candidate layout. Tool main can be selected with
`ARTPLAYER_THUMBNAIL_ARTIFACT`. Windows WebKit's HTTP-success/Blob-error-4 branch
is explicitly a capability control, not successful extraction. Full evidence:
`refactor/baselines/thumbnail-core-validation.json`; run
`yarn test:browser:source thumbnail-core.spec.js --workers=1`.
## Auto-thumbnail delayed registration
Auto-thumbnail's `auto-thumbnail-registration.spec.js` separately checks direct
+6 -1
View File
@@ -95,6 +95,11 @@ const vttCoreMember = `package/${vttCore.manifest.main.replace(/^\.\//, '')}`
const vttCoreBytes = readMember(await ensureArchive(vttCore), vttCoreMember)
assert.equal(hash(vttCoreBytes), vttCore.files[vttCoreMember], 'Historical VTT core differs from frozen archive')
add('/published-5.1.6/artplayer.js', vttCoreBytes, { kind: 'npm-release', version: vttCore.version, integrity: vttCore.integrity, member: vttCoreMember })
const thumbnailCore = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/thumbnail-core.json'), 'utf8')).release
const thumbnailCoreMember = `package/${thumbnailCore.manifest.main}`
const thumbnailCoreBytes = readMember(await ensureArchive(thumbnailCore), thumbnailCoreMember)
assert.equal(hash(thumbnailCoreBytes), thumbnailCore.files[thumbnailCoreMember], 'Historical Thumbnail core differs from frozen archive')
add('/published-3.5.31/artplayer.js', thumbnailCoreBytes, { kind: 'npm-release', version: thumbnailCore.version, integrity: thumbnailCore.integrity, member: thumbnailCoreMember })
for (const release of JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/multiple-subtitles-cores.json'), 'utf8')).releases) {
const member = `package/${release.manifest.main.replace(/^\.\//, '')}`
const bytes = readMember(await ensureArchive(release), member)
@@ -199,7 +204,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.2', 'published-5.1.6', 'published-5.1.7', 'published-5.3.0', 'published-5.3.1-beta.1'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
assert(['candidate', 'published', 'published-3.5.31', 'published-4.5.5', 'published-4.5.9', 'published-5.1.2', 'published-5.1.6', '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)
+172
View File
@@ -0,0 +1,172 @@
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { PNG } from 'pngjs'
import { hash } from '../../refactor/scripts/releases.mjs'
import { thumbnailCandidate, thumbnailHistorical } from '../helpers/thumbnail.js'
import { expect, test } from './fixtures.js'
const sample = fileURLToPath(new URL('./media/thumbnail-pattern.mp4', import.meta.url))
const candidate = await thumbnailCandidate()
const implementations = [
...thumbnailHistorical().filter(item => item.name !== 'workspace.legacy.js'),
{ ...candidate, lifecycle: true },
{ ...candidate, name: `${candidate.name}-workspace-policy`, compatibility: 'workspace-4.4', lifecycle: true },
]
for (const core of ['published-3.5.31', 'published', 'candidate']) {
for (const implementation of implementations) {
test(`Thumbnail ${implementation.name} sheets display and refresh in ${core} core`, async ({ page, browserName }, testInfo) => {
test.setTimeout(45000)
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: implementation.code })
const native = await page.evaluate(async () => {
const bytes = await (await fetch('/test/thumbnail-pattern.mp4')).blob()
const source = URL.createObjectURL(bytes)
const result = {}
for (const [transport, url] of [['http', '/test/thumbnail-pattern.mp4'], ['blob', source]]) {
const video = document.createElement('video')
video.muted = true
document.body.append(video)
result[transport] = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Native media control did not settle')), 5000)
video.onloadeddata = () => {
clearTimeout(timer)
resolve({ loaded: true, width: video.videoWidth })
}
video.onerror = () => {
clearTimeout(timer)
resolve({ loaded: false, code: video.error.code })
}
video.src = url
})
video.removeAttribute('src')
video.load()
video.remove()
}
URL.revokeObjectURL(source)
return result
})
const delay = implementation.legacy ? 300 : 20
await page.evaluate(({ compatibility, delay }) => {
const input = document.createElement('input')
input.id = 'thumbnail-file'
input.type = 'file'
document.body.append(input)
window.createdThumbnailUrls = []
window.revokedThumbnailUrls = []
const create = URL.createObjectURL.bind(URL)
const revoke = URL.revokeObjectURL.bind(URL)
URL.createObjectURL = (blob) => {
const url = create(blob)
window.createdThumbnailUrls.push(url)
return url
}
URL.revokeObjectURL = (url) => {
window.revokedThumbnailUrls.push(url)
revoke(url)
}
window.tool = new window.ArtplayerToolThumbnail({ fileInput: input, number: 12, column: 4, width: 160, height: 90, delay, compatibility })
window.toolErrors = []
window.tool.on('error', error => window.toolErrors.push(String(error)))
}, { compatibility: implementation.compatibility, delay })
await page.locator('#thumbnail-file').setInputFiles(sample)
await testInfo.attach('thumbnail-core-inputs', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, delay, toolSha256: hash(implementation.code), mediaSha256: hash(fs.readFileSync(sample)), native, originalToolArchiveAvailable: false }) })
if (!native.blob.loaded) {
expect(browserName).toBe('webkit')
expect(native).toEqual({ http: { loaded: true, width: 320 }, blob: { loaded: false, code: 4 } })
await expect.poll(() => page.evaluate(() => window.tool.video.error?.code)).toBe(4)
// Native error state can be set before the queued error event is dispatched.
await expect.poll(() => page.evaluate(() => window.toolErrors.length)).toBe(implementation.lifecycle ? 1 : 0)
await page.evaluate(() => window.tool.destroy())
testInfo.annotations.push({ type: 'capability-control', description: 'Native HTTP works; native Blob and tool Blob fail with media error 4. This case does not validate extraction or core thumbnail display.' })
return
}
await expect.poll(() => page.evaluate(() => window.tool.video.readyState)).toBeGreaterThanOrEqual(2)
const generations = []
for (let generation = 0; generation < 2; generation++) {
const sheet = await page.evaluate(async () => {
await window.tool.start()
const image = new Image()
image.src = window.tool.thumbnailUrl
await image.decode()
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
const context = canvas.getContext('2d')
context.drawImage(image, 0, 0)
const cells = Array.from({ length: 12 }, (_, index) => [...context.getImageData(index % 4 * 160 + 80, Math.floor(index / 4) * 90 + 45, 1, 1).data])
window.art = new window.Artplayer({
container: '.player',
url: window.tool.videoUrl,
muted: true,
thumbnails: { url: window.tool.thumbnailUrl, number: 12, column: 4, width: 160, height: 90, scale: 1 },
})
return { url: window.tool.thumbnailUrl, width: image.width, height: image.height, cells, png: canvas.toDataURL('image/png').split(',')[1] }
})
const sheetPng = PNG.sync.read(Buffer.from(sheet.png, 'base64'))
await testInfo.attach(`sheet-${generation}`, { contentType: 'image/png', body: Buffer.from(sheet.png, 'base64') })
delete sheet.png
expect([sheet.width, sheet.height]).toEqual([640, 300])
// Independent FFmpeg decoding confirms all twelve sample centers are blue.
for (const pixel of sheet.cells)
expect(pixel[2] - Math.max(pixel[0], pixel[1])).toBeGreaterThan(80)
await expect.poll(() => page.evaluate(() => window.art.template.$video.readyState)).toBeGreaterThanOrEqual(2)
const progress = page.locator('.art-control-progress')
const preview = page.locator('.art-control-thumbnails')
const bounds = await progress.boundingBox()
const observed = []
// The old core's first-cell/row-boundary defect is independently characterized.
for (const [index, oldIndex] of [[1, 1], [4, 3], [5, 5], [0, 5], [11, 11]]) {
const x = bounds.x + bounds.width * (index + 0.5) / 12
const y = bounds.y + bounds.height / 2
await page.mouse.move(x, y)
await expect.poll(async () => {
await page.mouse.move(x, y)
return preview.evaluate(element => element.style.backgroundImage)
}).toContain('blob:')
const cell = core === 'candidate' ? index : oldIndex
await expect(preview).toHaveCSS('background-position', `${-(cell % 4) * 160}px ${-Math.floor(cell / 4) * 90}px`)
await expect(preview).toBeVisible()
let screenshot
await expect.poll(async () => {
screenshot = await preview.screenshot()
const png = PNG.sync.read(screenshot)
expect([png.width, png.height]).toEqual([160, 90])
let difference = 0
let channels = 0
// The progress time tooltip covers the lower part of the preview.
// Compare the unobscured interior, excluding rounded border pixels.
for (let py = 8; py < 60; py++) {
for (let px = 8; px < 152; px++) {
const actual = (py * png.width + px) * 4
const expected = ((Math.floor(cell / 4) * 90 + py) * sheetPng.width + cell % 4 * 160 + px) * 4
for (let channel = 0; channel < 3; channel++) {
difference += Math.abs(png.data[actual + channel] - sheetPng.data[expected + channel])
channels++
}
}
}
return difference / channels
}).toBeLessThanOrEqual(3)
observed.push({ requestedIndex: index, displayedIndex: cell })
await testInfo.attach(`sheet-${generation}-cell-${index}`, { contentType: 'image/png', body: screenshot })
}
const beforeDestroy = await page.evaluate(() => window.revokedThumbnailUrls.slice())
await page.evaluate(() => window.art.destroy(true))
expect(await page.evaluate(() => window.revokedThumbnailUrls)).toEqual(beforeDestroy)
expect(await page.evaluate(() => window.tool.video.isConnected)).toBe(true)
generations.push({ ...sheet, observed })
}
expect(generations[0].url).not.toBe(generations[1].url)
const cleanup = await page.evaluate(() => {
window.tool.destroy()
return { created: window.createdThumbnailUrls, revoked: window.revokedThumbnailUrls, videoConnected: window.tool.video.isConnected, players: window.Artplayer.instances.length, errors: window.toolErrors }
})
expect(cleanup.revoked.filter(url => url === '')).toHaveLength(core === 'published' ? 2 : 0)
expect([...cleanup.created].sort()).toEqual(cleanup.revoked.filter(url => url !== '').sort())
expect(cleanup).toMatchObject({ videoConnected: false, players: 0, errors: [] })
await testInfo.attach('thumbnail-core-results', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, generations, cleanup, scope: 'Native local file extraction, real hover and screenshot pixels on two generations; historical core defects asserted separately; no physical device claim' }) })
})
}
}