From 4fa5c68be9d2d0d60d725c2a87b8d92a4dc8a85a Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Mon, 14 Sep 2026 09:36:10 +0800 Subject: [PATCH] test(auto-thumbnail): [PKG-AUTO-THUMB-03] verify first-seek diagnostic boundaries --- .../ARCHITECTURE.md | 8 + .../baselines/auto-thumbnail-first-seek.json | 2152 +++++++++++++++++ ...2026-09-14-PKG-AUTO-THUMB-03-first-seek.md | 56 + refactor/plan.md | 2 +- refactor/progress.md | 13 + refactor/risks.json | 6 +- .../auto-thumbnail-rendering-probe.mjs | 48 +- refactor/tasks.json | 4 +- 8 files changed, 2280 insertions(+), 9 deletions(-) create mode 100644 refactor/baselines/auto-thumbnail-first-seek.json create mode 100644 refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md diff --git a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md index 4501e06f6..ac4179582 100644 --- a/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md +++ b/packages/artplayer-plugin-auto-thumbnail/ARCHITECTURE.md @@ -150,6 +150,14 @@ correct Firefox control, so it is not a portable frame-readiness gate. The comma reports observations, not a passing acceptance suite, and never edits production bundles. See refactor's rendering-readiness record before repeating these options. +`yarn probe:auto-thumbnail-rendering --first-seek` separately compares initial +loadeddata drawing without a seek, one/two animation-frame waits, delayed post-seek +drawing and a completed forward/back seek. Native setter instrumentation records +actual seek targets; the warm variant must reach one second before seeking zero. +These variants still fail the unique-first-frame check on the recorded Windows +WebKit host. They are diagnostic page-owned callbacks, not production fixes or +cancellation code. See refactor's first-seek checkpoint before repeating them. + The frozen eight-second timeline has one unique purple first frame followed by red, black, blue and yellow sections. Its command and fingerprint are in `refactor/baselines/auto-thumbnail-timeline-media.json`. Reproduce into a new diff --git a/refactor/baselines/auto-thumbnail-first-seek.json b/refactor/baselines/auto-thumbnail-first-seek.json new file mode 100644 index 000000000..e6609496d --- /dev/null +++ b/refactor/baselines/auto-thumbnail-first-seek.json @@ -0,0 +1,2152 @@ +{ + "task": "PKG-AUTO-THUMB-03", + "status": "doing", + "baseline": "2f130c58f14b1f3ff847568ae9cf144a75f450fe", + "script": { + "file": "refactor/scripts/auto-thumbnail-rendering-probe.mjs", + "sha256": "deb4859d90106642806921452b4bde8add8abc9a007a28d637d83d93347e7891" + }, + "final": { + "file": "refactor/.cache/auto-thumbnail-first-seek-3Lx4HU/report.json", + "sha256": "6646be77435d85d663d57e46052f773b2a7e848ea9511d5285425755420e5f49", + "log": { + "file": "refactor/.cache/auto-thumbnail-first-seek-verified.log", + "sha256": "f353335e255378db7c43b21e75000cc5701949e4cca99dc5e2df6de75651304c", + "exitCode": 0 + }, + "capturedAt": "2026-09-14T01:34:27.011Z", + "environment": { + "node": "v24.21.0", + "platform": "win32", + "arch": "x64" + }, + "sourceSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302", + "mediaSha256": "10370156cffada462d3ea00aae0caadd62752369cd6538cc55248b3ddd7bf994", + "results": [ + { + "engine": "webkit", + "browser": "26.6", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "current", + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302", + "firstFrameMatches": false, + "outcome": "completed", + "draws": [ + { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.601947, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.203187, + "rgba": [ + 16, + 16, + 16, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.8021196, + "rgba": [ + 16, + 15, + 239, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4018972, + "rgba": [ + 235, + 236, + 12, + 255 + ], + "readyState": 3, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "loadeddata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0.0017428, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 1.601947, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 3.203187, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 4.8, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 4.8021196, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 6.4018972, + "readyState": 3, + "decoded": 0, + "total": 0 + } + ], + "warnings": [], + "seeks": [ + { + "target": 0, + "loadedData": 0, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": false + }, + { + "engine": "webkit", + "browser": "26.6", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "loaded-no-seek", + "codeSha256": "0133763db291e764e68e176ba812efb74ba44484cb4b6a129c3988e264deb4c5", + "firstFrameMatches": false, + "outcome": "completed", + "draws": [ + { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6023167, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.2010318, + "rgba": [ + 16, + 16, + 16, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.8018166, + "rgba": [ + 16, + 15, + 239, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4017129, + "rgba": [ + 235, + 236, + 12, + 255 + ], + "readyState": 3, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "loadeddata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0.0015839, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 1.6023167, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 3.2010318, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 4.8018166, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 6.4017129, + "readyState": 3, + "decoded": 0, + "total": 0 + } + ], + "warnings": [], + "seeks": [ + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": false + }, + { + "engine": "webkit", + "browser": "26.6", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "loaded-no-seek-raf", + "codeSha256": "dcc5e257a121797e7bc47393429ef673af244352aa0d74e4cab140eac8516362", + "firstFrameMatches": false, + "outcome": "completed", + "draws": [ + { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6015508, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.20158, + "rgba": [ + 16, + 16, + 16, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.8018519, + "rgba": [ + 16, + 15, + 239, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4014429, + "rgba": [ + 235, + 236, + 12, + 255 + ], + "readyState": 3, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "loadeddata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0.0024854, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 1.6015508, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 1.6015508, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 3.20158, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 4.8018519, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 6.4014429, + "readyState": 3, + "decoded": 0, + "total": 0 + } + ], + "warnings": [], + "seeks": [ + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": false + }, + { + "engine": "webkit", + "browser": "26.6", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "loaded-no-seek-two-raf", + "codeSha256": "5ae706b39c4238ad1f69b4d006529e22fd21eba67af72333abc9a8f684c2207a", + "firstFrameMatches": false, + "outcome": "completed", + "draws": [ + { + "time": 0, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6036271, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.2010334, + "rgba": [ + 16, + 16, + 16, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.801358, + "rgba": [ + 16, + 15, + 239, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4030411, + "rgba": [ + 235, + 236, + 12, + 255 + ], + "readyState": 3, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "loadeddata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0.0018133, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 1.6036271, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 3.2010334, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 4.801358, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 6.4030411, + "readyState": 3, + "decoded": 0, + "total": 0 + } + ], + "warnings": [], + "seeks": [ + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": false + }, + { + "engine": "webkit", + "browser": "26.6", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "seek-two-raf", + "codeSha256": "8ea80f6740f2caafc9a22d0a19e54b136505bb443939047fab1cd170672788a5", + "firstFrameMatches": false, + "outcome": "completed", + "draws": [ + { + "time": 0.0019222, + "rgba": [ + 0, + 0, + 0, + 0 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6018068, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.2018834, + "rgba": [ + 16, + 16, + 16, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.801805, + "rgba": [ + 16, + 15, + 239, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4020051, + "rgba": [ + 235, + 236, + 12, + 255 + ], + "readyState": 3, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "loadeddata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0.0019222, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 1.6018068, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 3.2018834, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 4.801805, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 6.4020051, + "readyState": 3, + "decoded": 0, + "total": 0 + } + ], + "warnings": [], + "seeks": [ + { + "target": 0, + "loadedData": 0, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": false + }, + { + "engine": "webkit", + "browser": "26.6", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "warm-backseek", + "codeSha256": "d626ea5a64bac572dd38254117ebf85bfd9cb0b3007e2b5de7b76c578db5b2ea", + "firstFrameMatches": false, + "outcome": "completed", + "draws": [ + { + "time": 0.0015944, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6026046, + "rgba": [ + 238, + 14, + 14, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.2024689, + "rgba": [ + 16, + 16, + 16, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.8037234, + "rgba": [ + 16, + 15, + 239, + 255 + ], + "readyState": 4, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.401974, + "rgba": [ + 235, + 236, + 12, + 255 + ], + "readyState": 3, + "decoded": 0, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "loadeddata", + "time": 1, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 1, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0.0013982, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 1, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 1.0012077, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0.0015944, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 1.6026046, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 1.6026046, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 3.2024689, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 4.8037234, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 6.401974, + "readyState": 3, + "decoded": 0, + "total": 0 + } + ], + "warnings": [], + "seeks": [ + { + "target": 1, + "loadedData": 0, + "readyState": 4 + }, + { + "target": 1, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 0, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": false + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "current", + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302", + "firstFrameMatches": true, + "outcome": "completed", + "draws": [ + { + "time": 0, + "rgba": [ + 127, + 0, + 126, + 255 + ], + "readyState": 4, + "decoded": 4, + "total": 4, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6, + "rgba": [ + 254, + 0, + 0, + 255 + ], + "readyState": 4, + "decoded": 26, + "total": 26, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.2, + "rgba": [ + 0, + 0, + 0, + 255 + ], + "readyState": 4, + "decoded": 36, + "total": 36, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.8, + "rgba": [ + 0, + 0, + 253, + 255 + ], + "readyState": 4, + "decoded": 64, + "total": 64, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4, + "rgba": [ + 255, + 255, + 2, + 255 + ], + "readyState": 4, + "decoded": 80, + "total": 80, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "loadeddata", + "time": 0, + "readyState": 1, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 1, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 0, + "readyState": 4, + "decoded": 4, + "total": 4 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "decoded": 4, + "total": 4 + }, + { + "name": "seeked", + "time": 1.6, + "readyState": 4, + "decoded": 26, + "total": 26 + }, + { + "name": "canplay", + "time": 1.6, + "readyState": 4, + "decoded": 26, + "total": 26 + }, + { + "name": "seeked", + "time": 3.2, + "readyState": 4, + "decoded": 36, + "total": 36 + }, + { + "name": "canplay", + "time": 3.2, + "readyState": 4, + "decoded": 36, + "total": 36 + }, + { + "name": "seeked", + "time": 4.8, + "readyState": 4, + "decoded": 64, + "total": 64 + }, + { + "name": "canplay", + "time": 4.8, + "readyState": 4, + "decoded": 64, + "total": 64 + }, + { + "name": "seeked", + "time": 6.4, + "readyState": 4, + "decoded": 80, + "total": 80 + }, + { + "name": "canplay", + "time": 6.4, + "readyState": 4, + "decoded": 80, + "total": 80 + } + ], + "warnings": [], + "seeks": [ + { + "target": 0, + "loadedData": 0, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": true + }, + { + "engine": "chromium", + "browser": "153.0.8010.12", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "warm-backseek", + "codeSha256": "d626ea5a64bac572dd38254117ebf85bfd9cb0b3007e2b5de7b76c578db5b2ea", + "firstFrameMatches": true, + "outcome": "completed", + "draws": [ + { + "time": 0, + "rgba": [ + 127, + 0, + 126, + 255 + ], + "readyState": 4, + "decoded": 8, + "total": 8, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6, + "rgba": [ + 254, + 0, + 0, + 255 + ], + "readyState": 4, + "decoded": 30, + "total": 30, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.2, + "rgba": [ + 0, + 0, + 0, + 255 + ], + "readyState": 4, + "decoded": 40, + "total": 40, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.8, + "rgba": [ + 0, + 0, + 253, + 255 + ], + "readyState": 4, + "decoded": 68, + "total": 68, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4, + "rgba": [ + 255, + 255, + 2, + 255 + ], + "readyState": 4, + "decoded": 84, + "total": 84, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 4, + "decoded": 0, + "total": 0 + }, + { + "name": "loadeddata", + "time": 1, + "readyState": 1, + "decoded": 0, + "total": 0 + }, + { + "name": "canplay", + "time": 1, + "readyState": 1, + "decoded": 0, + "total": 0 + }, + { + "name": "seeked", + "time": 1, + "readyState": 4, + "decoded": 4, + "total": 4 + }, + { + "name": "canplay", + "time": 0, + "readyState": 1, + "decoded": 4, + "total": 4 + }, + { + "name": "seeked", + "time": 0, + "readyState": 4, + "decoded": 8, + "total": 8 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "decoded": 8, + "total": 8 + }, + { + "name": "seeked", + "time": 1.6, + "readyState": 4, + "decoded": 30, + "total": 30 + }, + { + "name": "canplay", + "time": 1.6, + "readyState": 4, + "decoded": 30, + "total": 30 + }, + { + "name": "seeked", + "time": 3.2, + "readyState": 4, + "decoded": 40, + "total": 40 + }, + { + "name": "canplay", + "time": 3.2, + "readyState": 4, + "decoded": 40, + "total": 40 + }, + { + "name": "seeked", + "time": 4.8, + "readyState": 4, + "decoded": 68, + "total": 68 + }, + { + "name": "canplay", + "time": 4.8, + "readyState": 4, + "decoded": 68, + "total": 68 + }, + { + "name": "seeked", + "time": 6.4, + "readyState": 4, + "decoded": 84, + "total": 84 + }, + { + "name": "canplay", + "time": 6.4, + "readyState": 4, + "decoded": 84, + "total": 84 + } + ], + "warnings": [], + "seeks": [ + { + "target": 1, + "loadedData": 0, + "readyState": 4 + }, + { + "target": 0, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": true + }, + { + "engine": "firefox", + "browser": "155.0", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "current", + "codeSha256": "6080dd90bde239515646899bdd8e71d17de24119523e4883025fbf39b1a6a302", + "firstFrameMatches": true, + "outcome": "completed", + "draws": [ + { + "time": 0, + "rgba": [ + 137, + 19, + 126, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6, + "rgba": [ + 255, + 24, + 0, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.2, + "rgba": [ + 0, + 0, + 0, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.8, + "rgba": [ + 0, + 14, + 253, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4, + "rgba": [ + 255, + 241, + 2, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 1, + "total": 0 + }, + { + "name": "loadeddata", + "time": 0, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 0, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 1.6, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 1.6, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 3.2, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 3.2, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 4.8, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 4.8, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 6.4, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 6.4, + "readyState": 4, + "total": 0 + } + ], + "warnings": [], + "seeks": [ + { + "target": 0, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": true + }, + { + "engine": "firefox", + "browser": "155.0", + "style": "hidden", + "css": "visibility:hidden;", + "readiness": "warm-backseek", + "codeSha256": "d626ea5a64bac572dd38254117ebf85bfd9cb0b3007e2b5de7b76c578db5b2ea", + "firstFrameMatches": true, + "outcome": "completed", + "draws": [ + { + "time": 0, + "rgba": [ + 137, + 19, + 126, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 1.6, + "rgba": [ + 255, + 24, + 0, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 3.2, + "rgba": [ + 0, + 0, + 0, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 4.8, + "rgba": [ + 0, + 14, + 253, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + }, + { + "time": 6.4, + "rgba": [ + 255, + 241, + 2, + 255 + ], + "readyState": 4, + "total": 0, + "box": { + "width": 320, + "height": 180 + } + } + ], + "events": [ + { + "name": "loadedmetadata", + "time": 0, + "readyState": 1, + "total": 0 + }, + { + "name": "loadeddata", + "time": 0, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 1, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 1, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 0, + "readyState": 1, + "total": 0 + }, + { + "name": "canplay", + "time": 0, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 1.6, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 1.6, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 3.2, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 3.2, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 4.8, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 4.8, + "readyState": 4, + "total": 0 + }, + { + "name": "seeked", + "time": 6.4, + "readyState": 4, + "total": 0 + }, + { + "name": "canplay", + "time": 6.4, + "readyState": 4, + "total": 0 + } + ], + "warnings": [], + "seeks": [ + { + "target": 1, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 0, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 1.6, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 3.2, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 4.8, + "loadedData": 1, + "readyState": 4 + }, + { + "target": 6.4, + "loadedData": 1, + "readyState": 4 + } + ], + "supportsFrames": true + } + ], + "scope": "Diagnostic capture, not acceptance. WebKit baseline, first loadeddata drawing without seek, one/two animation-frame waits, two-frame post-seek wait and warm forward/back seek. Native currentTime setter instrumentation includes every actual seek. Chromium/Firefox current-code controls retain native presentation callbacks. Production files remain unchanged; pending diagnostic RAF callbacks are page-owned, not a production cancellation implementation." + }, + "ordinaryMode": { + "file": "refactor/.cache/auto-thumbnail-rendering-HuntLy/report.json", + "sha256": "448ac0a37f214ffe63f77fd9ccc4a8df066cb1a66a4d37998d0b618f3a5edb55", + "log": { + "file": "refactor/.cache/auto-thumbnail-first-seek-default-mode.log", + "sha256": "79aa69f87a201c395ada62d013306382aa59e4ecc1bcf6f8e49e8d72c0f95c77", + "exitCode": 0 + }, + "profiles": 14, + "outcomes": [ + { + "engine": "webkit", + "style": "hidden", + "readiness": "current", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "hidden", + "readiness": "ready-state", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "hidden", + "readiness": "loaded-event", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "opacity", + "readiness": "current", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "opacity", + "readiness": "ready-state", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "opacity", + "readiness": "loaded-event", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "clip", + "readiness": "current", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "clip", + "readiness": "ready-state", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "clip", + "readiness": "loaded-event", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "visible", + "readiness": "current", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "visible", + "readiness": "ready-state", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "webkit", + "style": "visible", + "readiness": "loaded-event", + "outcome": "completed", + "firstFrameMatches": false + }, + { + "engine": "chromium", + "style": "hidden", + "readiness": "current", + "outcome": "completed", + "firstFrameMatches": true + }, + { + "engine": "firefox", + "style": "hidden", + "readiness": "current", + "outcome": "completed", + "firstFrameMatches": true + } + ] + }, + "intermediate": [ + { + "file": "refactor/.cache/auto-thumbnail-first-seek-YDpgJA/report.json", + "sha256": "14965a28e98778d0a2a87388400fbe2a055655dc8827767e534be9cc55e289cd", + "limit": "Warm attempt falsely completed on a stale zero event; actual setter trace has no backward seek. Not a valid warm/back comparison." + }, + { + "file": "refactor/.cache/auto-thumbnail-first-seek-zog3fO/report.json", + "sha256": "573c83fc1d29fe6b79e5154f81eabe79399c5757fa138968aad56859aec4a414", + "limit": "Warm seek waits for target correctly but does not retry stale events; WebKit hit eight-second diagnostic timeout without a draw." + } + ], + "protectedPathsUnchanged": [ + "packages/artplayer-plugin-auto-thumbnail/src", + "packages/artplayer-plugin-auto-thumbnail/types", + "packages/artplayer-plugin-auto-thumbnail/dist", + "test/browser/auto-thumbnail-pixels.spec.js" + ], + "conclusion": "All six final WebKit profiles miss the unique first purple frame; Chromium/Firefox baseline and warm controls retain it. Do not adopt tested variants as production fixes. No package completion or device acceptance." +} diff --git a/refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md b/refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md new file mode 100644 index 000000000..a69c5139c --- /dev/null +++ b/refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md @@ -0,0 +1,56 @@ +# PKG-AUTO-THUMB-03: first-seek diagnostic checkpoint + +Baseline: `2f130c58f14b1f3ff847568ae9cf144a75f450fe`. The task and +AUTO-THUMB-PIXEL-01 remain open. This change adds diagnostic evidence, not a +production workaround or package acceptance. + +Earlier rendering/readiness experiments could not distinguish the first +`currentTime = 0` seek from initial loadeddata presentation. The new +`yarn probe:auto-thumbnail-rendering --first-seek` mode compares six Windows +WebKit profiles: current code; first draw from loadeddata without a seek; +that same draw after one/two animation frames; two animation frames after seek; +and a completed forward seek to one second followed by a seek to zero. + +Each code replacement must match exactly one known boundary. Native currentTime +getter/setter instrumentation delegates to HTMLMediaElement and records every +actual seek, including the warm-up seek and retries. It does not pretend that +all variants seek five times. The sample retains its unique purple first frame, +later red/black/blue/yellow cells and fixed SHA-256. Chromium/Firefox current-code +controls (baseline and forward/back seek) retain native frame callbacks. There is no new production timeout, +playback call, pixel substitution or test skip. + +All six WebKit profiles completed five updates, but none obtained the required +purple first frame. Omitting the zero seek therefore does not establish correct +initial pixels. Animation-frame waits and warm forward/back seeking also cannot +be adopted as fixes on this evidence. Both engine controls obtained the purple +frame. This is a host/version-specific observation, not proof of a WebKit-wide +or physical Safari defect. + +The first exploratory cached script did not record the warm-up seek at its +actual setter. Setter instrumentation then exposed an invalid warm-up comparison: +a late zero-time event completed the first draw before any backward seek. The +final probe waits for the one-second seek to complete before seeking zero, with +at most three retries of a mismatched warm seek. Without those retries WebKit +never completed the warm seek within the eight-second diagnostic window. The +final observed targets start `[1, 1, 0]` in WebKit and `[1, 0]` in both controls; +the probe checks that only warm retries precede zero. It was rerun; +only its final report is the authoritative first-seek comparison. Code and media +fingerprints, every actual seek and first-cell results are retained in +`../baselines/auto-thumbnail-first-seek.json`; raw event/draw reports remain in +the referenced cache. The ordinary rendering mode is also rerun because its +instrumentation shares this change. A successful process exit means diagnostic +capture completed, not that extraction passed pixel acceptance. + +These in-memory variants are page-lifetime experiments; their RAF callbacks do +not constitute production cancellation handling. Package sources, declarations, +dist and default extraction remain unchanged. Existing native pixel assertions +are neither removed nor weakened. Do not reintroduce these failed variants as +untested suggestions. The next investigation needs a different decoded-frame +signal or supported-device evidence; independent plugin/core combination tasks +can proceed while this issue remains open. + +Validation is scoped to the changed diagnostic: targeted lint, both actual +browser probe modes, plan/risk consistency and risk-register tests. Full package +and global tests are not rerun because no production or acceptance-test behavior +changes. Revert this checkpoint to remove the extra probe mode and its records. +Local checkpoint commit only; no task completion, push or publication. diff --git a/refactor/plan.md b/refactor/plan.md index b669ad5a1..e11f9d57e 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -528,7 +528,7 @@ - PKG-AUDIO-05: [记录](changes/2026-09-12-PKG-AUDIO-05-combinations.md) [记录](baselines/audio-combinations-first.json) [记录](baselines/audio-combinations-checkpoint.json) [记录](audio-validation.md) - 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) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-rendering-readiness.md) [记录](baselines/auto-thumbnail-rendering-readiness.json) +- 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) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-rendering-readiness.md) [记录](baselines/auto-thumbnail-rendering-readiness.json) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md) [记录](baselines/auto-thumbnail-first-seek.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-VTT-THUMB-02: [记录](baselines/vtt-thumbnail-failures.json) [记录](changes/2026-09-13-PKG-VTT-THUMB-02-failures.md) - PKG-VTT-THUMB-03: [记录](changes/2026-09-13-PKG-VTT-THUMB-03-resources.md) [记录](baselines/vtt-thumbnail-resources.json) [记录](changes/2026-09-13-PKG-VTT-THUMB-03-parser.md) [记录](baselines/vtt-thumbnail-parser.json) diff --git a/refactor/progress.md b/refactor/progress.md index 5ec04ba21..e305237d5 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,18 @@ # 进度与证据 + +## Auto Thumbnail 首次 seek 诊断检查点 + +SITE-04 的全包文档核对依赖尚未完成的 Auto Thumbnail/VAST 迁移,本轮先回到 +PKG-AUTO-THUMB-03。新增 --first-seek 诊断,保留唯一紫色首帧和真实 setter +记录。最终十组中 WebKit 六组均完成抽帧但首帧错误;Chromium/Firefox 各两组 +控制正确。去掉首次 seek、延迟绘制与核实完成的前进/回零都不能作为修复。 +迟到零点事件使最初 warm 探针没有实际回零;已修正观测并保留无效/超时报告。 +普通十四组诊断也完成;生产源码、产物和像素断言未改。任务/风险仍开放, +不是整包验收通过。见[检查点](changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md)和[证据](baselines/auto-thumbnail-first-seek.json)。 +任务数保持 243 / 170 done。下一步推进已满足依赖的 VTT Thumbnail 新旧核心 +组合验收;SITE-04 保持 todo,不跳过其依赖。本地检查点提交,无推送/发布。 + ## SITE-03 桌面编辑器与生成流程完成 桌面自有 UI 拆为严格 TS 的 bootstrap、Monaco、运行会话、文件导入与设置模块; diff --git a/refactor/risks.json b/refactor/risks.json index 495d84782..c7b83ac53 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4185,9 +4185,11 @@ "test/auto-thumbnail-frames.test.js", "refactor/baselines/auto-thumbnail-frame-presentation.json", "refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-rendering-readiness.md", - "refactor/baselines/auto-thumbnail-rendering-readiness.json" + "refactor/baselines/auto-thumbnail-rendering-readiness.json", + "refactor/changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md", + "refactor/baselines/auto-thumbnail-first-seek.json" ], - "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. Intrinsic-size hidden/opacity/clip/visible rendering crossed with current/readyState/first-loadeddata-event strategies still misses the unique first frame in all 12 Windows WebKit profiles. Correct Firefox frames retain totalVideoFrames=0, so that counter cannot be a portable readiness gate. Do not reintroduce these unvalidated workarounds.", + "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. Intrinsic-size hidden/opacity/clip/visible rendering crossed with current/readyState/first-loadeddata-event strategies still misses the unique first frame in all 12 Windows WebKit profiles. Correct Firefox frames retain totalVideoFrames=0, so that counter cannot be a portable readiness gate. Do not reintroduce these unvalidated workarounds. First-seek diagnostics additionally exclude drawing the first loadeddata frame without seek, one/two RAF waits, post-seek RAF waits and verified forward/back seeking on the recorded Windows WebKit host. The warm comparison first had an invalid stale-event completion, then a timeout; final bounded retries verify actual [1,1,0] seeking and still miss purple. Controls retain purple. These are exclusions, not production fixes.", "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." }, diff --git a/refactor/scripts/auto-thumbnail-rendering-probe.mjs b/refactor/scripts/auto-thumbnail-rendering-probe.mjs index 14cf87996..7d98955fc 100644 --- a/refactor/scripts/auto-thumbnail-rendering-probe.mjs +++ b/refactor/scripts/auto-thumbnail-rendering-probe.mjs @@ -7,9 +7,11 @@ import { chromium, firefox, webkit } from '@playwright/test' const bundle = fs.readFileSync('packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs', 'utf8') const bytes = fs.readFileSync('test/browser/media/auto-thumbnail-timeline.mp4') +const firstSeek = process.argv.includes('--first-seek') +assert(process.argv.slice(2).every(arg => arg === '--first-seek'), 'Unknown rendering probe argument') assert.equal(process.version.slice(1), fs.readFileSync('.node-version', 'utf8').trim()) assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn probe:auto-thumbnail-rendering') -const output = fs.mkdtempSync('refactor/.cache/auto-thumbnail-rendering-') +const output = fs.mkdtempSync(`refactor/.cache/auto-thumbnail-${firstSeek ? 'first-seek' : 'rendering'}-`) console.log(`Rendering diagnostics: ${output}`) const fingerprint = data => createHash('sha256').update(data).digest('hex') assert.equal(fingerprint(bytes), JSON.parse(fs.readFileSync('refactor/baselines/auto-thumbnail-timeline-media.json')).file.sha256, 'The unique-frame diagnostic requires the frozen timeline fixture') @@ -32,6 +34,10 @@ const server = http.createServer((request, response) => { await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) const styles = { hidden: 'visibility:hidden;', opacity: 'visibility:visible;opacity:0;', clip: 'visibility:visible;clip-path:inset(50%);', visible: 'visibility:visible;' } const results = [] +function replaceOnce(code, before, after) { + assert.equal(code.split(before).length, 2, `Expected one probe boundary: ${before}`) + return code.replace(before, after) +} function purple([red, green, blue, alpha]) { return alpha === 255 && Math.min(red, blue) > 100 && Math.abs(red - blue) < 30 && green < 40 } @@ -40,8 +46,10 @@ try { const browser = await engine.launch() try { const variants = engineName === 'webkit' - ? Object.entries(styles).flatMap(([style, css]) => ['current', 'ready-state', 'loaded-event'].map(readiness => ({ style, css, readiness }))) - : [{ style: 'hidden', css: styles.hidden, readiness: 'current' }] + ? firstSeek + ? ['current', 'loaded-no-seek', 'loaded-no-seek-raf', 'loaded-no-seek-two-raf', 'seek-two-raf', 'warm-backseek'].map(readiness => ({ style: 'hidden', css: styles.hidden, readiness })) + : Object.entries(styles).flatMap(([style, css]) => ['current', 'ready-state', 'loaded-event'].map(readiness => ({ style, css, readiness }))) + : (firstSeek ? ['current', 'warm-backseek'] : ['current']).map(readiness => ({ style: 'hidden', css: styles.hidden, readiness })) for (const variant of variants) { const page = await browser.newPage() await page.goto(`http://127.0.0.1:${server.address().port}`) @@ -50,7 +58,22 @@ try { code = code.replace('const ready = !presentedFrames || video.readyState >= 2;', 'const ready = video.readyState >= 2;') if (variant.readiness === 'loaded-event') code = code.replace('let pending;', 'let pending; let firstFrame = true;').replace('const ready = !presentedFrames || video.readyState >= 2;', 'const ready = !firstFrame; firstFrame = false;') - code = code.replace('video.currentTime = target;', '{ window.__thumbnailSeeks.push({target, loadedData: window.__thumbnailData, readyState: video.readyState}); video.currentTime = target; }') + if (variant.readiness.startsWith('loaded-no-seek')) { + code = replaceOnce(code, 'let pending;', 'let pending; let firstFrame = true;') + code = replaceOnce(code, 'const ready = !presentedFrames || video.readyState >= 2;', 'const ready = !firstFrame;') + const finish = variant.readiness === 'loaded-no-seek-two-raf' + ? 'requestAnimationFrame(() => requestAnimationFrame(() => finish()))' + : variant.readiness === 'loaded-no-seek-raf' + ? 'requestAnimationFrame(() => finish())' + : 'finish()' + code = replaceOnce(code, 'current.started = true;', `current.started = true; if (firstFrame) { firstFrame = false; current.sought = true; current.presented = true; ${finish}; return; }`) + } + if (variant.readiness === 'seek-two-raf') + code = replaceOnce(code, 'current.sought = true;\n finish();', 'current.sought = true; requestAnimationFrame(() => requestAnimationFrame(() => finish()));') + if (variant.readiness === 'warm-backseek') { + code = replaceOnce(code, 'let pending;', 'let pending; let warm = false; let warmRetries = 0;') + code = replaceOnce(code, 'current.started = true;', 'if (!warm && target === 0) { warm = true; video.onseeked = job.guard(() => { if (!active() || video.seeking) return; if (Math.abs(video.currentTime - 1) >= 0.05) { if (++warmRetries > 3) throw new Error("Warm seek did not reach one second"); video.currentTime = 1; return; } video.onseeked = null; begin(); }); video.currentTime = 1; return; } current.started = true;') + } const result = await page.evaluate(async (code) => { const draws = [] const events = [] @@ -67,6 +90,15 @@ try { const element = create(...args) if (args[0] === 'video') { videos.push(element) + const time = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'currentTime') + Object.defineProperty(element, 'currentTime', { + configurable: true, + get() { return time.get.call(this) }, + set(target) { + window.__thumbnailSeeks.push({ target, loadedData: window.__thumbnailData, readyState: this.readyState }) + time.set.call(this, target) + }, + }) element.addEventListener('loadeddata', () => window.__thumbnailData++) for (const name of ['loadedmetadata', 'loadeddata', 'canplay', 'seeked', 'playing']) element.addEventListener(name, () => events.push({ name, time: element.currentTime, readyState: element.readyState, decoded: element.webkitDecodedFrameCount, total: element.getVideoPlaybackQuality?.().totalVideoFrames })) } @@ -118,6 +150,12 @@ try { console.warn = originalWarn } }, code) + if (variant.readiness === 'warm-backseek' && result.outcome === 'completed') { + const targets = result.seeks.map(seek => seek.target) + assert.equal(targets[0], 1, 'Warm comparison must start with a forward seek') + assert(targets.indexOf(0) > 0, 'Warm comparison must actually seek back before drawing') + assert(targets.slice(0, targets.indexOf(0)).every(target => target === 1), 'Only bounded warm retries may precede the backward seek') + } const firstFrameMatches = result.draws[0] ? purple(result.draws[0].rgba) : false const entry = { engine: engineName, browser: browser.version(), ...variant, codeSha256: fingerprint(code), firstFrameMatches, ...result } results.push(entry) @@ -131,5 +169,5 @@ try { finally { await new Promise(resolve => server.close(resolve)) assert.equal(fs.readFileSync('packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs', 'utf8'), bundle) - fs.writeFileSync(`${output}/report.json`, `${JSON.stringify({ capturedAt: new Date().toISOString(), environment: { node: process.version, platform: process.platform, arch: process.arch }, sourceSha256: fingerprint(bundle), mediaSha256: fingerprint(bytes), results, scope: 'Diagnostic capture, not acceptance. Four intrinsic-size rendering modes and three first-frame readiness strategies are compared in WebKit on the recorded host. Chromium/Firefox current-code controls retain native presentation callbacks. No production file is changed; missing or incorrect pixels remain explicit.' }, null, 2)}\n`) + fs.writeFileSync(`${output}/report.json`, `${JSON.stringify({ capturedAt: new Date().toISOString(), environment: { node: process.version, platform: process.platform, arch: process.arch }, sourceSha256: fingerprint(bundle), mediaSha256: fingerprint(bytes), results, scope: firstSeek ? 'Diagnostic capture, not acceptance. WebKit baseline, first loadeddata drawing without seek, one/two animation-frame waits, two-frame post-seek wait and warm forward/back seek. Native currentTime setter instrumentation includes every actual seek. Chromium/Firefox current-code controls retain native presentation callbacks. Production files remain unchanged; pending diagnostic RAF callbacks are page-owned, not a production cancellation implementation.' : 'Diagnostic capture, not acceptance. Four intrinsic-size rendering modes and three first-frame readiness strategies are compared in WebKit on the recorded host. Chromium/Firefox current-code controls retain native presentation callbacks. No production file is changed; missing or incorrect pixels remain explicit.' }, null, 2)}\n`) } diff --git a/refactor/tasks.json b/refactor/tasks.json index 63a309123..2617c1a3b 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1799,7 +1799,9 @@ "changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md", "baselines/auto-thumbnail-frame-presentation.json", "changes/2026-09-14-PKG-AUTO-THUMB-03-rendering-readiness.md", - "baselines/auto-thumbnail-rendering-readiness.json" + "baselines/auto-thumbnail-rendering-readiness.json", + "changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md", + "baselines/auto-thumbnail-first-seek.json" ] }, {