test(auto-thumbnail): [PKG-AUTO-THUMB-03] verify first-seek diagnostic boundaries

This commit is contained in:
Harvey Zhao committed 2026-09-14 09:36:10 +08:00
1 parent 2f130c58f1
commit 4fa5c68be9
8 files changed
+2280 -9

No files matched your search

@@ -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
File diff suppressed because it is too large. Load diff
@@ -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.
+1 -1
View File
@@ -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)
+13
View File
@@ -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、运行会话、文件导入与设置模块;
+4 -2
View File
@@ -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."
},
@@ -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`)
}
+3 -1
View File
@@ -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"
]
},
{