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, 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. 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, The bilingual VitePress audio-track guides explain the actual deferred update,
strict drift threshold, URL truthiness and shared mute behavior. Keep their Run Code 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 source identical to docs/assets/example/audio.track.js. document-audio.spec.js
+11
View File
@@ -1,5 +1,16 @@
# Audio Track 行为与媒体测试 # 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 候选修复 ## PKG-AUDIO-03 候选修复
音频状态已从核心事件接入拆出,源码 44 项、加入三种产物共 89 项 Node 通过。 音频状态已从核心事件接入拆出,源码 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`,不要手改本表。 > 由 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。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 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-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-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-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-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-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 | | 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-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-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-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-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 ## 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) - 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) - 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) - 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 下载候选内容核验 ## CI-NPM-02 下载候选内容核验
新增`release:verify-bundle`,以独立提供的源码SHA、manifest摘要、包批次/tag 新增`release:verify-bundle`,以独立提供的源码SHA、manifest摘要、包批次/tag
+5 -3
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1, "schemaVersion": 1,
"task": "BASE-07", "task": "BASE-07",
"sourceCommit": "e01f39ff", "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.", "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": [ "items": [
{ {
@@ -3005,9 +3005,11 @@
"refactor/changes/2026-09-15-CI-01-source-webkit.md", "refactor/changes/2026-09-15-CI-01-source-webkit.md",
"refactor/baselines/ci-source-webkit-validation.json", "refactor/baselines/ci-source-webkit-validation.json",
"refactor/changes/2026-09-15-CI-01-installed-webkit.md", "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": "受影响的缓冲/恢复得到真实媒体证据;与原生能力和测试夹具差异分开,发布环境的剩余范围经显式处理。", "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." "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": [ "dependsOn": [
"PKG-AUDIO-04", "PKG-AUDIO-04",
"CORE-22", "CORE-22",
"CORE-24" "CORE-24",
"PKG-AUDIO-BUFFER-01"
], ],
"status": "doing", "status": "doing",
"risk": "H", "risk": "H",
@@ -6159,6 +6160,26 @@
"changes/2026-09-16-CI-NPM-02-verify-bundle.md", "changes/2026-09-16-CI-NPM-02-verify-bundle.md",
"baselines/npm-bundle-verification.json" "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() 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 core of ['published', 'candidate']) {
for (const plugin of ['published', 'source']) { 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']) { for (const owner of ['video', 'audio']) {
test(`${core} core + ${plugin} audio: real ${owner} starvation resumes after network release`, async ({ page }, testInfo) => { 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 file = owner === 'video' ? 'pattern.mp4' : 'audio-tone.m4a'
const bytes = fs.readFileSync(new URL(`./media/${file}`, import.meta.url)) const bytes = fs.readFileSync(new URL(`./media/${file}`, import.meta.url))
const limit = owner === 'video' ? 96 * 1024 : 32 * 1024 const limit = owner === 'video' ? 96 * 1024 : 32 * 1024
const gate = await createMediaGate(bytes, owner === 'video' ? 'video/mp4' : 'audio/mp4', limit) const gate = await createMediaGate(bytes, owner === 'video' ? 'video/mp4' : 'audio/mp4', limit)
let phase = 'initial-playback'
try { try {
await openAudio(page, core, plugin, testInfo, owner === 'audio' ? gate.url : '/test/audio-tone.m4a') await openAudio(page, core, plugin, testInfo, owner === 'audio' ? gate.url : '/test/audio-tone.m4a')
await page.evaluate((owner) => { await page.evaluate((owner) => {
@@ -25,8 +81,10 @@ for (const core of ['published', 'candidate']) {
}, owner) }, owner)
await page.locator('#play').click() await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime > 0.3 && window.art.currentTime > 0.3)).toBe(true) await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime > 0.3 && window.art.currentTime > 0.3)).toBe(true)
if (owner === 'video') if (owner === 'video') {
await page.evaluate(url => window.art.switchUrl(url), gate.url) 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) 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) expect(gate.blocked).toBeGreaterThan(0)
const stalled = await page.evaluate(() => ({ audio: window.audioPlugin.audio.currentTime, video: window.art.currentTime })) 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) await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(stalled.video + 0.3)
expect(await page.evaluate(() => window.art.video.paused)).toBe(false) expect(await page.evaluate(() => window.art.video.paused)).toBe(false)
} }
phase = 'network-release'
gate.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.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.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(() => 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) 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]) expect(await page.evaluate(() => [window.art.video.error, window.audioPlugin.audio.error])).toEqual([null, null])
phase = 'complete'
} }
finally { finally {
try { try {
await testInfo.attach('media-gate-requests', { contentType: 'application/json', body: JSON.stringify({ file, sha256: hash(bytes), bytes: bytes.length, limit, requests: gate.requests }) }) 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) 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 }) }) 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()) if (!page.isClosed())
await page.evaluate(() => window.art?.destroy()) await page.evaluate(() => window.art?.destroy())
} }