test(multiple-subtitles): checkpoint native seek diagnostics [PKG-MULTI-SUB-05]

This commit is contained in:
Harvey Zhao committed 2026-09-14 14:03:22 +08:00
1 parent 318de37e8a
commit a8d19ce5ef
10 files changed
+7266 -8

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,63 @@
# PKG-MULTI-SUB-05 checkpoint: source-seek diagnostics (not complete)
Task11 is committed as318de37e8 and its commit audit passed. This checkpoint
continues the reopened old-core source-seek issue without changing production
code, declarations, dependencies or built artifacts.
## Added evidence
The optional diagnostics capture native events, public seek/fullscreen events,
monotonic times, native position/duration/seeking, active cues and switch Promise
settlement. Full mode forwards the original native currentTime/playbackRate
accessors and records writes/stacks; events mode leaves those properties intact.
Both modes are opt-in. Diagnostic side observers handle rejection while the
original returned Promise still rejects for the test consumer.
Initial WebKit serial3 and repeated9 cases pass their caption assertions. The
repeated traces show all six old-core switches resolve while seeking=true;
restoration seeked is delivered30..50ms later. The three candidate switches wait
for restoration. No playback-rate assignment was observed in these source steps.
The successful later user writes occurred after the prior native seeked event.
The actual-main events-only matrix passes105 assertions, including explicitly
labelled historical overlap/offset defect observations. It again shows early
old-core settlement, including WebKit native seeked43/46ms later. This is not
proof the previously missed seeks were repaired: observation may affect timing,
and no failing native write trace was captured in those runs.
The subsequent actual-main matrix with diagnostics disabled has62 passes and one
failure on old5.3.0/WebKit. At failure, media time is0.0015139, the1..3-second cue
is intact, active cues are empty, and the player is still alive. The candidate
and other old-core cases pass in that run. This reproduces the same near-zero
position category previously seen on old5.4.0; it does not implicate ASS conversion
or prove the precise internal native overwrite sequence.
## Separate restoration-event control
`source-restoration.js` installs a native metadata listener before switchUrl.
After the core's metadata handler, it waits for native seeked if seeking is active,
and also waits for the original switch Promise. It cleans both listeners on
completion or failure. There are no fixed delays, seek retries or source rewrites.
It is enabled only with ARTPLAYER_SOURCE_RESTORE_EVENT=1; the default failing
property-based path and existing immediate-call ordering probe remain available.
With diagnostics disabled, the event control passes27 cases: three repetitions
of three core inputs across three desktop engines using actual main artifacts.
This supports independently verifying captions after native restoration. It is
not proof of a production fix, or sufficient evidence to close the original
immediate-seek failure. Do not make the normal path green by relabelling this
control as the original public API behavior.
Reports, input/artifact hashes, selected modes, timings and remaining failures are
in [the checkpoint evidence](../baselines/multiple-subtitles-seek-diagnostics.json).
The browser README documents flags. Explicit test/helper lint, CI50 and plan/risk
checks pass. No source or package build was needed for these test-only additions.
## Remaining work
MULTI-SUB-SWITCH-01 stays open and task05 stays doing. The next diagnostic step is
to capture the failed default path's restoration completion versus subsequent
native seek without assuming that property polling equals event completion.
Keep candidate public switch guarantees separate from immutable old-host defects;
do not patch the shared media clock from the subtitle plugin. Broader historical
profile/device acceptance and task06 distribution gates remain incomplete.
+1 -1
View File
@@ -558,7 +558,7 @@
- PKG-MULTI-SUB-04: [记录](baselines/multiple-subtitles-runtime-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md) [记录](baselines/multiple-subtitles-public-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md) [记录](type-compatibility-policy.md) [记录](baselines/multiple-subtitles-approved-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md)
- PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
- PKG-MULTI-SUB-08: [记录](changes/2026-09-14-PKG-MULTI-SUB-08-entities.md) [记录](baselines/multiple-subtitles-entities-validation.json)
- PKG-MULTI-SUB-05: [记录](changes/2026-09-14-PKG-MULTI-SUB-05-combinations.md) [记录](baselines/multiple-subtitles-combinations-validation.json) [记录](baselines/multiple-subtitles-cores.json)
- PKG-MULTI-SUB-05: [记录](changes/2026-09-14-PKG-MULTI-SUB-05-combinations.md) [记录](baselines/multiple-subtitles-combinations-validation.json) [记录](baselines/multiple-subtitles-cores.json) [记录](changes/2026-09-14-PKG-MULTI-SUB-05-seek-diagnostics.md) [记录](baselines/multiple-subtitles-seek-diagnostics.json)
- PKG-JASSUB-01: [记录](baselines/jassub-release.json) [记录](baselines/jassub-vendor.json) [记录](baselines/jassub-font-metadata.json) [记录](baselines/jassub-contract.md) [记录](baselines/jassub-contract-validation.json) [记录](changes/2026-09-13-PKG-JASSUB-01-baseline.md) [记录](baselines/jassub-provenance.json) [记录](baselines/jassub-provenance-validation.json) [记录](changes/2026-09-14-PKG-JASSUB-01-provenance.md)
- PKG-JASSUB-02: [记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md) [记录](baselines/jassub-failures-validation.json)
- PKG-JASSUB-03: [记录](changes/2026-09-14-PKG-JASSUB-03-registration.md) [记录](baselines/jassub-registration-validation.json)
+13
View File
@@ -1,5 +1,18 @@
# 进度与证据
## PKG-MULTI-SUB-05 切源时序诊断检查点(仍未完成)
新增可选原生属性/事件跟踪和独立恢复事件等待对照。WebKit串行3项、重复9项及
三引擎被动事件矩阵105项通过断言,但旧核心仍提前结算,WebKit恢复seeked晚30至50ms;
候选核心会等待恢复。关闭诊断后63项中62通过,旧5.3.0/WebKit再次停在0.0015139秒,
字幕cue仍完整;没有获得失败瞬间的原生写入轨迹,不能据带诊断的通过结果关闭风险。
显式等待metadata后的原生恢复seeked对照27项通过,仅说明完成恢复后的字幕路径有效,
不修改默认测试路径或旧公共API。即时调用探针、此前失败和MULTI-SUB-SWITCH-01保持open。
见[检查点](changes/2026-09-14-PKG-MULTI-SUB-05-seek-diagnostics.md)和
[证据](baselines/multiple-subtitles-seek-diagnostics.json)。本轮仅测试和文档修改,产物未变,
脚本lint/CI50/计划风险检查通过;仍为179 done、17 doing、55 todo。task05/06未完成。
保留原生失败时序定位入口;下一轮转回Auto Thumbnail剩余源码整理,再继续包级组合门槛。
## PKG-MULTI-SUB-11 旧发布核心 ASS 转换适配完成
实际5.1.2 tarball证明只有main转换器丢失模板换行,同版legacy与源码正确;新增
+7 -3
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"task": "BASE-07",
"sourceCommit": "e01f39ff",
"updatedAt": "2026-09-14T05:41:02.580Z",
"updatedAt": "2026-09-14T06:00:35.506Z",
"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": [
{
@@ -6034,7 +6034,7 @@
"id": "MULTI-SUB-SWITCH-01",
"title": "Intermittent WebKit core5.3.0 caption loss after selected-track source switch",
"confirmation": "reproduced",
"workspaceState": "Reopened during task10: both source and actual-main matrices fail old5.4.0/WebKit caption after switch despite waiting seeking=false. Source failure native time0.001494, intact1..3 cue, active=[]; task09 attribution remains supported but the later-seek workaround is insufficient. Candidate core and old5.1 display regressions pass. Task05 must diagnose remaining old-host ordering, without hiding these failures.",
"workspaceState": "Still open after task11: property traces3+9 and passive105 cases pass while confirming early old-core settlement; default untraced63 has another near-zero seek miss on5.3.0/WebKit (time0.0015139, intact1..3 cue, no active cue). No failing native-write trace yet. Opt-in metadata/restoration-seeked control passes27 cases but is not a production/API fix; default and immediate-call probes remain unchanged. Candidate source guarantees pass. Task05 still owns remaining diagnosis and acceptance.",
"compatibleResolution": "Classify the old-host source/native-seek race separately from caption rendering; preserve immediate-call probes, require candidate first seeks, and wait for real native source completion before independent caption-seek assertions.",
"closureCriteria": "Root cause classified with targeted evidence, any required fix validated, and changed candidate/old-core combination rerun without hiding the original failure.",
"status": "open",
@@ -6052,7 +6052,11 @@
"refactor/baselines/multiple-subtitles-switch-validation.json",
"test/browser/multiple-subtitles-switch.spec.js",
"refactor/changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md",
"refactor/baselines/multiple-subtitles-legacy-validation.json"
"refactor/baselines/multiple-subtitles-legacy-validation.json",
"refactor/changes/2026-09-14-PKG-MULTI-SUB-05-seek-diagnostics.md",
"refactor/baselines/multiple-subtitles-seek-diagnostics.json",
"test/browser/source-seek-diagnostics.js",
"test/browser/source-restoration.js"
],
"resolutionEvidence": [
"refactor/changes/2026-09-14-PKG-MULTI-SUB-09-switch-order.md",
+3 -1
View File
@@ -2417,7 +2417,9 @@
"evidence": [
"changes/2026-09-14-PKG-MULTI-SUB-05-combinations.md",
"baselines/multiple-subtitles-combinations-validation.json",
"baselines/multiple-subtitles-cores.json"
"baselines/multiple-subtitles-cores.json",
"changes/2026-09-14-PKG-MULTI-SUB-05-seek-diagnostics.md",
"baselines/multiple-subtitles-seek-diagnostics.json"
]
},
{
+18
View File
@@ -283,3 +283,21 @@ multiline text/HTML, overlapping translation, selection/reset and empty interval
on5.1.2/5.1.7/5.3.0/5.4.0/candidate. The attachment records the unchanged original
host converter output alongside actual native cues and browser/artifact hashes.
It covers desktop video/track playback, not full ASS layout or physical devices.
The multiple-subtitles combination suite has opt-in source-seek diagnostics:
- `ARTPLAYER_SOURCE_SEEK_TRACE=events` records native and public events, clocks,
duration, active cues and switch settlement without replacing media properties.
- `ARTPLAYER_SOURCE_SEEK_TRACE=1` additionally forwards native `currentTime` and
`playbackRate` accessors while recording writes/stacks. The page owns these
diagnostic listeners and descriptors; do not use this helper in production.
- `ARTPLAYER_SOURCE_RESTORE_EVENT=1` selects a separate control that installs a
metadata listener before switching and, when restoration is seeking, waits for
its native seeked event as well as the original switch Promise. This tests caption
behavior after restoration; it does not repair the old public switch contract.
With no flags, the original Promise plus seeking-property path remains unchanged.
The immediate-call ordering probe also remains intact. Record the chosen flags
alongside artifacts: diagnostic overhead may change an intermittent race, and
passing the event control must not replace the original failure evidence. The
combination attachment includes the selected modes and optional native trace.
@@ -1,6 +1,9 @@
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { multipleSubtitlesCandidate, multipleSubtitlesHistorical } from '../helpers/multiple-subtitles.js'
import { expect, test } from './fixtures.js'
import { switchAfterNativeRestoration } from './source-restoration.js'
import { installSeekDiagnostics } from './source-seek-diagnostics.js'
const candidate = await multipleSubtitlesCandidate()
const historical = (await multipleSubtitlesHistorical()).filter(item => /^published-.*-main$/.test(item.name))
@@ -15,7 +18,7 @@ test.afterEach(async ({ page }, testInfo) => {
return null
const track = art.template.$track?.track
const read = cue => ({ start: cue.startTime, end: cue.endTime, text: cue.text })
return { destroyed: art.isDestroy, paused: art.video.paused, time: art.currentTime, offset: art.subtitleOffset, cues: Array.from(track?.cues || [], read), active: Array.from(track?.activeCues || [], read), displayed: art.template.$subtitle?.textContent }
return { destroyed: art.isDestroy, paused: art.video.paused, time: art.currentTime, seeking: art.video.seeking, duration: art.video.duration, readyState: art.video.readyState, offset: art.subtitleOffset, cues: Array.from(track?.cues || [], read), active: Array.from(track?.activeCues || [], read), displayed: art.template.$subtitle?.textContent, trace: window.sourceSeekTrace }
})
await testInfo.attach('multiple-combination-final-state', { contentType: 'application/json', body: JSON.stringify(state) })
})
@@ -27,7 +30,9 @@ async function prepare(page, core, implementation, browser, testInfo) {
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.multipleFactory = module.exports.default || module.exports; })();` })
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await testInfo.attach('multiple-combination-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, pluginSha256: hash(implementation.code), browser: browser.version(), scope: 'Desktop engines; 5.3.0 is adjacent stable, not the unavailable 5.3.1 release; no minimum-version or physical-device claim' }) })
if (['1', 'events'].includes(process.env.ARTPLAYER_SOURCE_SEEK_TRACE))
await installSeekDiagnostics(page, process.env.ARTPLAYER_SOURCE_SEEK_TRACE === '1')
await testInfo.attach('multiple-combination-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, pluginSha256: hash(implementation.code), browser: browser.version(), traceMode: process.env.ARTPLAYER_SOURCE_SEEK_TRACE || 'off', sourceRestoration: process.env.ARTPLAYER_SOURCE_RESTORE_EVENT === '1' ? 'native-event-control' : 'promise-and-seeking-property', scope: 'Desktop engines; 5.3.0 is adjacent stable, not the unavailable 5.3.1 release; no minimum-version or physical-device claim' }) })
}
async function seek(page, time) {
@@ -94,7 +99,22 @@ for (const core of cores.filter(core => !['published-5.1.2', 'published-5.1.7'].
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['b']))
await expect(page.locator('.art-subtitle-a')).toHaveCount(0)
await expect(page.locator('.art-subtitle-b')).toHaveText('Translation')
await page.evaluate(() => window.art.switchUrl('/test/pattern.mp4?subtitles-combination'))
if (process.env.ARTPLAYER_SOURCE_RESTORE_EVENT === '1') {
await switchAfterNativeRestoration(page, '/test/pattern.mp4?subtitles-combination')
}
else {
await page.evaluate(() => {
window.recordSourceSeek?.('before-switch')
const pending = window.art.switchUrl('/test/pattern.mp4?subtitles-combination')
if (window.recordSourceSeek) {
void pending.then(
() => window.recordSourceSeek('switch-resolved'),
error => window.recordSourceSeek('switch-rejected', String(error)),
)
}
return pending
})
}
// Old core promises can settle during their native restoration seek.
// The immediate-seek path is retained separately in multiple-subtitles-switch.spec.js.
await expect.poll(() => page.evaluate(() => window.art.video.seeking)).toBe(false)
+31
View File
@@ -0,0 +1,31 @@
export async function switchAfterNativeRestoration(page, url) {
await page.evaluate((url) => {
const art = window.art
if (url === art.url)
return art.switchUrl(url)
const video = art.video
let resolveRestored
const restored = new Promise((resolve) => {
resolveRestored = resolve
})
const onSeeked = () => resolveRestored()
const onMetadata = () => {
if (video.seeking)
video.addEventListener('seeked', onSeeked, { once: true })
else
resolveRestored()
}
const cleanup = () => {
video.removeEventListener('loadedmetadata', onMetadata)
video.removeEventListener('seeked', onSeeked)
}
video.addEventListener('loadedmetadata', onMetadata, { once: true })
try {
return Promise.all([art.switchUrl(url), restored]).then(() => undefined).finally(cleanup)
}
catch (error) {
cleanup()
throw error
}
}, url)
}
+27
View File
@@ -0,0 +1,27 @@
export async function installSeekDiagnostics(page, properties = true) {
await page.evaluate((properties) => {
const art = window.art
const video = art.video
window.sourceSeekTrace = []
const state = () => ({ clock: performance.now(), time: video.currentTime, duration: video.duration, seeking: video.seeking, paused: video.paused, rate: video.playbackRate, readyState: video.readyState, source: video.currentSrc, active: Array.from(art.template.$track.track.activeCues || [], cue => cue.text) })
const record = (event, detail) => window.sourceSeekTrace.push({ event, detail, ...state() })
for (const property of properties ? ['currentTime', 'playbackRate'] : []) {
const descriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, property)
Object.defineProperty(video, property, {
configurable: true,
get() { return descriptor.get.call(this) },
set(value) {
record(`before:${property}`, { value, stack: new Error('Native media property write').stack })
descriptor.set.call(this, value)
record(`after:${property}`, { value })
},
})
}
for (const event of ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'seeking', 'seeked', 'emptied', 'pause', 'ratechange', 'timeupdate'])
video.addEventListener(event, () => record(`native:${event}`))
for (const event of ['restart', 'seek', 'fullscreen'])
art.on(event, (...values) => record(`art:${event}`, values))
window.recordSourceSeek = record
record('installed')
}, properties)
}