test(audio): [PKG-AUDIO-BUFFER-01] observe switches while network responses are held

This commit is contained in:
Harvey Zhao committed 2026-09-16 01:19:58 +08:00
1 parent 6f7fd7931d
commit aa146f07e4
9 files changed
+5299 -10

No files matched your search

@@ -65,6 +65,14 @@ source failure recovery, and old/native pause/end plus corrected candidate clean
See refactor/audio-validation.md for failed baseline probes, Windows WebKit WAV/size limits,
physical-device gaps and the remaining 05/06 combination/distribution gates.
`audio-buffering.spec.js` observes switchUrl's Promise independently while its real
HTTP response is held, and requires fulfillment after release. Never await that
Promise before allowing the test to release the network. A separate regression
sends complete MP4 format/metadata boxes while withholding frames to exercise this
ordering; it does not replace the existing trusted waiting and playback recovery
assertions. Phase/switch/media attachments distinguish a pending switch from the
Windows WebKit native buffering gap, which remains open.
The bilingual VitePress audio-track guides explain the actual deferred update,
strict drift threshold, URL truthiness and shared mute behavior. Keep their Run Code
source identical to docs/assets/example/audio.track.js. document-audio.spec.js
+11
View File
@@ -1,5 +1,16 @@
# Audio Track 行为与媒体测试
## PKG-AUDIO-BUFFER-01:断流测试的等待顺序
`audio-buffering.spec.js`不再先等待switchUrl完成才检查/放行网络,改为记录其
pending/fulfilled/rejected状态,放行后明确要求成功。新增完整MP4元数据但扣住
全部编码帧的回归:旧等待方式20秒超时,修正后的新旧核心/插件三浏览器12项通过。
仅1字节的初版夹具曾触发Firefox媒体错误,失败保留;最终按ftyp/moov结构发出
1605字节。此回归验证测试可继续放行网络,不代替真实缓冲断言。
原断流矩阵仍16通过、Windows WebKit8失败,全部到达native-starvation阶段,
没有放宽trusted waiting、双时钟恢复或同步检查。详见[记录](changes/2026-09-16-PKG-AUDIO-BUFFER-01-order.md)
与[证据](baselines/audio-buffer-order-validation.json)。Audio-05和AUDIO-BUFFER-01继续开放。
## PKG-AUDIO-03 候选修复
音频状态已从核心事件接入拆出,源码 44 项、加入三种产物共 89 项 Node 通过。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,46 @@
# PKG-AUDIO-BUFFER-01: observe source switching while the network is held
The buffering test awaited `switchUrl` before checking starvation and releasing
the withheld response. Switching can await `canplay` and resumed playback, so
this could stop the test from releasing the bytes needed to complete the switch.
Earlier installed WebKit traces stopped at this await. The initial current-source
rerun instead reached the existing native-starvation failure; these observations
are preserved separately.
`beginAudioSwitch` now starts the real switch without returning its Promise to
Playwright. It records pending/fulfilled/rejected status and timestamps. The test
can observe the actual HTTP gate while switching, then requires fulfillment after
release. Rejections cannot produce a passing result. Original trusted `waiting`,
nonzero time, stalled-audio behavior, recovery, synchronization and media-error
assertions remain. Phase, switch and media observations are attached before cleanup.
A new regression withholds every encoded frame while sending complete `ftyp` and
`moov` boxes from the existing MP4: 1,605 bytes derived from box sizes. It requires
a held request and pending switch, then releases the network and checks fulfillment
and resumed video/audio clocks. This tests orchestration, not ongoing playback
starvation. An initial one-byte fixture caused all four Firefox combinations to
reject with a native media error; that failed run is retained. The final fixture
sends complete format/metadata boxes instead of accepting those errors.
With the final fixture, reinstating the exact old await expression reproduced a
20-second Chromium timeout in `begin-switch`, with 1,605 bytes sent and the request
held. The corrected source was restored byte for byte. All 12 corrected cases
pass: published/current core crossed with published/current Audio Track in
Chromium 153.0.8010.12, Firefox 155.0 and Windows WebKit 26.6, without retry/skip.
The separate original starvation matrix remains 16 passed and 8 failed. All
failures are Windows WebKit at the trusted native-starvation assertion; two video
cases have pending switches and two have fulfilled switches. Completion and
starvation therefore remain separate observations. AUDIO-BUFFER-01 is not waived.
The final metadata correction changes only the new case; original starvation
inputs/assertions and the shared helper retain the tested bytes. These separate
runs must not be described as a fully green 36-case suite.
Canonical Node 24.21.0/Yarn 1.22.22 were used. Three real HTTP gate unit tests and
read-only spec lint pass. The [validation record](../baselines/audio-buffer-order-validation.json)
retains run summaries, input provenance, report hashes and phase/media/request
evidence. Candidate browser inputs are source builds, not installed npm tarballs.
No production API/code, media fixture, gate server, timeout, dependency or
distribution changed. This closes test ordering only; PKG-AUDIO-05/06 and native
buffering/device acceptance remain outstanding. No formal review, push,
deployment or publication was performed.
+6 -4
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 284 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 285 项,范围 22 个包及工作区/示例。
状态:todo 41 / doing 20 / blocked 0 / done 223 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 41 / doing 20 / blocked 0 / done 224 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -18,7 +18,7 @@
| artplayer-plugin-ads | 2.1.0 | PKG-ADS-01, PKG-ADS-02, PKG-ADS-03, PKG-ADS-04, PKG-ADS-05, PKG-ADS-06 |
| artplayer-plugin-ambilight | 1.1.0 | PKG-AMBILIGHT-01, PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-04, PKG-AMBILIGHT-PROXY-01, PKG-AMBILIGHT-05, PKG-AMBILIGHT-06, PKG-FACTORY-01 |
| artplayer-plugin-asr | 2.1.0 | PKG-ASR-01, PKG-ASR-02, PKG-ASR-03, PKG-ASR-04, PKG-ASR-05, PKG-ASR-06, PKG-ASR-07, PKG-ASR-08, PKG-ASR-09, PKG-ASR-10 |
| artplayer-plugin-audio-track | 1.1.0 | PKG-AUDIO-01, PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-04, PKG-AUDIO-05, PKG-AUDIO-06, SITE-AUDIO-01, CI-TYPES-01 |
| artplayer-plugin-audio-track | 1.1.0 | PKG-AUDIO-01, PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-04, PKG-AUDIO-05, PKG-AUDIO-06, SITE-AUDIO-01, CI-TYPES-01, PKG-AUDIO-BUFFER-01 |
| artplayer-plugin-auto-thumbnail | 1.1.0 | PKG-AUTO-THUMB-01, PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-06, PKG-AUTO-THUMB-07, PKG-AUTO-THUMB-08, PKG-AUTO-THUMB-09, PKG-AUTO-THUMB-10, PKG-AUTO-THUMB-11, PKG-AUTO-THUMB-12, PKG-AUTO-THUMB-13 |
| artplayer-plugin-chapter | 1.1.0 | PILOT-01, PKG-CHAPTER-01, PKG-CHAPTER-02, PKG-CHAPTER-03, PKG-CHAPTER-04, PKG-CHAPTER-05, PKG-CHAPTER-06, PKG-CHAPTER-HOVER-01 |
| artplayer-plugin-chromecast | 1.1.0 | PKG-CAST-01, PKG-CAST-02, PKG-CAST-03, PKG-CAST-04, PKG-CAST-05, PKG-CAST-06 |
@@ -174,8 +174,9 @@
| PKG-AUDIO-02 | artplayer-plugin-audio-track<br>建立特有行为与错误测试 | PKG-AUDIO-01, ENG-03, ENG-05 | 偏移、倍率、seek、waiting/playing、加载错误和 autoplay 拒绝 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-AUDIO-03 | artplayer-plugin-audio-track<br>整理内部职责与资源 | PKG-AUDIO-02, CORE-10 | 外部 audio 的同步策略、监听与源更新生命周期 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-AUDIO-04 | artplayer-plugin-audio-track<br>迁移自有源码和公开类型 | PKG-AUDIO-03, ENG-04, ENG-06, CORE-07 | Option/Result/audio、update 输入和事件类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-AUDIO-05 | artplayer-plugin-audio-track<br>验证新旧核心和组合 | PKG-AUDIO-04, CORE-22, CORE-24 | 主视频切源和缓冲恢复,多实例独立音频、旧核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing |
| PKG-AUDIO-05 | artplayer-plugin-audio-track<br>验证新旧核心和组合 | PKG-AUDIO-04, CORE-22, CORE-24, PKG-AUDIO-BUFFER-01 | 主视频切源和缓冲恢复,多实例独立音频、旧核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing |
| PKG-AUDIO-06 | artplayer-plugin-audio-track<br>验证分发并同步文档 | PKG-AUDIO-05, ENG-07 | audio.track.js 示例与运行返回值一致的声明 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
| PKG-AUDIO-BUFFER-01 | artplayer-plugin-audio-track<br>修正断流测试的切源等待顺序 | PKG-AUDIO-04, CORE-22 | 网络扣留时独立观察切源Promise;完整元数据扣帧回归与阶段诊断 | 旧顺序复现死等,三浏览器新旧组合可放行并验证恢复;保留真实缓冲失败,不以本项关闭Audio-05或设备门槛 | M | done |
## 5 包迁移:artplayer-plugin-auto-thumbnail
@@ -746,3 +747,4 @@
- DOC-REVIEW-01: [记录](changes/2026-09-15-DOC-REVIEW-01-user-guidance.md) [记录](release-reviews.md) [记录](ai-workflow.md)
- PKG-DANMUKU-START-01: [记录](baselines/ci-installed-webkit-validation.json) [记录](changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md) [记录](baselines/danmuku-start-validation.json)
- CI-NPM-02: [记录](changes/2026-09-16-CI-NPM-02-verify-bundle.md) [记录](baselines/npm-bundle-verification.json)
- PKG-AUDIO-BUFFER-01: [记录](changes/2026-09-16-PKG-AUDIO-BUFFER-01-order.md) [记录](baselines/audio-buffer-order-validation.json)
+10
View File
@@ -1,5 +1,15 @@
# 进度与证据
## PKG-AUDIO-BUFFER-01 断流测试等待顺序修复
修正switchUrl与网络放行互相等待的问题;完整MP4元数据/无编码帧的回归用旧顺序
复现20秒死等,修正后新旧核心/插件三浏览器12项通过,HTTP gate单测3项和lint通过。
原真实断流矩阵仍16通过、Windows WebKit8失败,未放宽断言或修改生产行为。
初版1字节夹具导致Firefox错误的失败报告也保留;最终按媒体结构发送完整元数据。
见[记录](changes/2026-09-16-PKG-AUDIO-BUFFER-01-order.md)和[证据](baselines/audio-buffer-order-validation.json)。
本子项done,Audio-05/06与AUDIO-BUFFER-01保持未完成;224 done、20 doing、41 todo,
共285项。没有复盘、推送、部署或发布。
## CI-NPM-02 下载候选内容核验
新增`release:verify-bundle`,以独立提供的源码SHA、manifest摘要、包批次/tag
+5 -3
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"task": "BASE-07",
"sourceCommit": "e01f39ff",
"updatedAt": "2026-09-14T07:23:15.393Z",
"updatedAt": "2026-09-15T17:18:32.442Z",
"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": [
{
@@ -3005,9 +3005,11 @@
"refactor/changes/2026-09-15-CI-01-source-webkit.md",
"refactor/baselines/ci-source-webkit-validation.json",
"refactor/changes/2026-09-15-CI-01-installed-webkit.md",
"refactor/baselines/ci-installed-webkit-validation.json"
"refactor/baselines/ci-installed-webkit-validation.json",
"refactor/changes/2026-09-16-PKG-AUDIO-BUFFER-01-order.md",
"refactor/baselines/audio-buffer-order-validation.json"
],
"compatibleResolution": "保留真实 waiting 断言;使用独立原生对照和其他实际支持环境/输入继续验证,不将没有播放推进的诊断当成缓冲通过,也不凭此改变插件 API 或否定真实 Safari 支持。",
"compatibleResolution": "保留真实 waiting 断言;使用独立原生对照和其他实际支持环境/输入继续验证,不将没有播放推进的诊断当成缓冲通过,也不凭此改变插件 API 或否定真实 Safari 支持。 PKG-AUDIO-BUFFER-01已修正测试等待顺序;独立切源放行12项通过,原WebKit真实缓冲8项仍失败,本风险不关闭。",
"closureCriteria": "受影响的缓冲/恢复得到真实媒体证据;与原生能力和测试夹具差异分开,发布环境的剩余范围经显式处理。",
"workspaceState": "CI-01 九包安装扩展的首轮 336 项中,Windows WebKit 八项新旧音频受限媒体组合继续失败。七项未观察到目标可信 waiting,一项受限视频路径耗尽整体时间并在最终状态读取中报页面关闭。其他引擎均通过;保留原始失败,不以销毁后状态反推初始解码错误,不推断新的共同底层原因。 Same-file prefix controls now show Windows WebKit can start before releasing the tail with larger prefixes (video188653/audio65536), but four committed native diagnostics still record no trusted waiting. Default three-engine diagnostics retain Chromium/Firefox8 progression+waiting and WebKit4 no progression/no waiting. Before-release request copies distinguish held bytes from final sent counters; no production fix or waiver. Complete WebKit source run ae56937d3: all eight initial dual-clock assertions pass, then all eight fail the original trusted waiting/time>0.3 assertion at line30. Seven record no waiting; one has only a trusted time0 event. Held prefixes remain96KiB/32KiB. Recovery assertions are not reached; no production fix or waiver."
},
+22 -1
View File
@@ -1756,7 +1756,8 @@
"dependsOn": [
"PKG-AUDIO-04",
"CORE-22",
"CORE-24"
"CORE-24",
"PKG-AUDIO-BUFFER-01"
],
"status": "doing",
"risk": "H",
@@ -6159,6 +6160,26 @@
"changes/2026-09-16-CI-NPM-02-verify-bundle.md",
"baselines/npm-bundle-verification.json"
]
},
{
"id": "PKG-AUDIO-BUFFER-01",
"phase": "5 包迁移:artplayer-plugin-audio-track",
"title": "修正断流测试的切源等待顺序",
"scope": [
"artplayer-plugin-audio-track"
],
"dependsOn": [
"PKG-AUDIO-04",
"CORE-22"
],
"status": "done",
"risk": "M",
"deliverable": "网络扣留时独立观察切源Promise;完整元数据扣帧回归与阶段诊断",
"acceptance": "旧顺序复现死等,三浏览器新旧组合可放行并验证恢复;保留真实缓冲失败,不以本项关闭Audio-05或设备门槛",
"evidence": [
"changes/2026-09-16-PKG-AUDIO-BUFFER-01-order.md",
"baselines/audio-buffer-order-validation.json"
]
}
]
}
+73 -2
View File
@@ -6,14 +6,70 @@ import { expect, test } from './fixtures.js'
const { openAudio } = useAudioFixture()
async function beginAudioSwitch(page, url) {
// switchUrl may await canplay/play; do not return its pending Promise to
// Playwright before the test can observe or release the real network gate.
await page.evaluate((url) => {
window.starvationSwitch = { status: 'pending', startedAt: performance.now() }
window.art.switchUrl(url).then(() => {
window.starvationSwitch.status = 'fulfilled'
window.starvationSwitch.settledAt = performance.now()
}, (error) => {
window.starvationSwitch.status = 'rejected'
window.starvationSwitch.error = String(error)
window.starvationSwitch.settledAt = performance.now()
})
}, url)
}
for (const core of ['published', 'candidate']) {
for (const plugin of ['published', 'source']) {
test(`${core} core + ${plugin} audio: held switch can release before its Promise settles`, async ({ page }, testInfo) => {
const bytes = fs.readFileSync(new URL('./media/pattern.mp4', import.meta.url))
const metadata = bytes.readUInt32BE(0)
expect(bytes.toString('ascii', 4, 8)).toBe('ftyp')
expect(bytes.toString('ascii', metadata + 4, metadata + 8)).toBe('moov')
// Send complete format/metadata boxes, but withhold all encoded frames.
const limit = metadata + bytes.readUInt32BE(metadata)
const gate = await createMediaGate(bytes, 'video/mp4', limit)
let phase = 'initial-playback'
try {
await openAudio(page, core, plugin, testInfo)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime > 0.3 && window.audioPlugin.audio.currentTime > 0.3)).toBe(true)
phase = 'begin-switch'
await beginAudioSwitch(page, gate.url)
phase = 'held-request'
await expect.poll(() => gate.blocked).toBeGreaterThan(0)
expect(await page.evaluate(() => window.starvationSwitch.status)).toBe('pending')
expect(gate.requests.some(request => request.wasHeld && request.sent === limit)).toBe(true)
phase = 'network-release'
gate.release()
await expect.poll(() => page.evaluate(() => window.starvationSwitch)).toMatchObject({ status: 'fulfilled' })
await expect.poll(() => page.evaluate(() => window.art.currentTime > 0.3 && window.audioPlugin.audio.currentTime > 0.3 && !window.art.video.paused && !window.audioPlugin.audio.paused)).toBe(true)
expect(await page.evaluate(() => [window.art.video.error, window.audioPlugin.audio.error])).toEqual([null, null])
phase = 'complete'
}
finally {
try {
const state = page.isClosed() ? { closed: true } : await page.evaluate(() => ({ switch: window.starvationSwitch, hostEvents: window.audioHostEvents }))
await testInfo.attach('held-switch', { contentType: 'application/json', body: JSON.stringify({ phase, ...state, sha256: hash(bytes), limit, requests: gate.requests }) })
if (!page.isClosed())
await page.evaluate(() => window.art?.destroy())
}
finally {
await gate.close()
}
}
})
for (const owner of ['video', 'audio']) {
test(`${core} core + ${plugin} audio: real ${owner} starvation resumes after network release`, async ({ page }, testInfo) => {
const file = owner === 'video' ? 'pattern.mp4' : 'audio-tone.m4a'
const bytes = fs.readFileSync(new URL(`./media/${file}`, import.meta.url))
const limit = owner === 'video' ? 96 * 1024 : 32 * 1024
const gate = await createMediaGate(bytes, owner === 'video' ? 'video/mp4' : 'audio/mp4', limit)
let phase = 'initial-playback'
try {
await openAudio(page, core, plugin, testInfo, owner === 'audio' ? gate.url : '/test/audio-tone.m4a')
await page.evaluate((owner) => {
@@ -25,8 +81,10 @@ for (const core of ['published', 'candidate']) {
}, owner)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime > 0.3 && window.art.currentTime > 0.3)).toBe(true)
if (owner === 'video')
await page.evaluate(url => window.art.switchUrl(url), gate.url)
if (owner === 'video') {
await beginAudioSwitch(page, gate.url)
}
phase = 'native-starvation'
await expect.poll(() => page.evaluate(() => window.starvation.some(event => event.trusted && event.time > 0.3 && event.readyState < 3))).toBe(true)
expect(gate.blocked).toBeGreaterThan(0)
const stalled = await page.evaluate(() => ({ audio: window.audioPlugin.audio.currentTime, video: window.art.currentTime }))
@@ -37,18 +95,31 @@ for (const core of ['published', 'candidate']) {
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(stalled.video + 0.3)
expect(await page.evaluate(() => window.art.video.paused)).toBe(false)
}
phase = 'network-release'
gate.release()
if (owner === 'video') {
await expect.poll(() => page.evaluate(() => window.starvationSwitch)).toMatchObject({ status: 'fulfilled' })
}
phase = 'playback-recovery'
await expect.poll(() => page.evaluate(() => !window.audioPlugin.audio.paused && !window.art.video.paused)).toBe(true)
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(stalled.audio + 0.3)
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(stalled.video + 0.3)
await expect.poll(() => page.evaluate(() => Math.abs(window.audioPlugin.audio.currentTime - window.art.currentTime))).toBeLessThan(0.5)
expect(await page.evaluate(() => [window.art.video.error, window.audioPlugin.audio.error])).toEqual([null, null])
phase = 'complete'
}
finally {
try {
await testInfo.attach('media-gate-requests', { contentType: 'application/json', body: JSON.stringify({ file, sha256: hash(bytes), bytes: bytes.length, limit, requests: gate.requests }) })
const events = page.isClosed() ? { closed: true } : await page.evaluate(() => window.starvation)
await testInfo.attach('media-gate', { contentType: 'application/json', body: JSON.stringify({ file, sha256: hash(bytes), bytes: bytes.length, limit, requests: gate.requests, events }) })
const state = page.isClosed()
? { closed: true }
: await page.evaluate(() => {
const media = element => element && ({ time: element.currentTime, paused: element.paused, readyState: element.readyState, ended: element.ended, error: element.error?.code ?? null })
return { switch: window.starvationSwitch, video: media(window.art?.video), audio: media(window.audioPlugin?.audio), hostEvents: window.audioHostEvents }
})
await testInfo.attach('audio-buffering-phase', { contentType: 'application/json', body: JSON.stringify({ phase, ...state }) })
if (!page.isClosed())
await page.evaluate(() => window.art?.destroy())
}