test(audio): [PKG-AUDIO-05] distinguish prefix startup from native starvation

This commit is contained in:
Harvey Zhao committed 2026-09-15 13:12:05 +08:00
1 parent bf56d09178
commit 3eb5500525
9 files changed
+4151 -11

No files matched your search

+12
View File
@@ -124,3 +124,15 @@ Chromium/Firefox 的八种组合都观测到推进与可信 waiting,Windows We
不支持缓冲;更不能为候选忽略 failing 用例。AUDIO-BUFFER-01 保持 open,Audio-05 不支持缓冲;更不能为候选忽略 failing 用例。AUDIO-BUFFER-01 保持 open,Audio-05
仍 doing;需要其他可实际执行的原生环境/输入路径补齐。当前没有已安装 WSL 发行版或 仍 doing;需要其他可实际执行的原生环境/输入路径补齐。当前没有已安装 WSL 发行版或
可用 Docker 命令。物理设备、声音输出、自动播放策略和 Audio-06 的安装/demo 仍未验收。 可用 Docker 命令。物理设备、声音输出、自动播放策略和 Audio-06 的安装/demo 仍未验收。
### PKG-AUDIO-05 同文件字节前缀对照
后续[前缀诊断](changes/2026-09-15-PKG-AUDIO-05-prefix.md)保留相同 MP4/AAC、
媒体哈希和原有集成断言。原生 Windows WebKit 在更大的前缀下可以于尾部仍被扣住时
推进时钟,说明之前的“不推进”不是这两个格式一概不能渐进起播。四个完整模式
诊断仍未见可信 waiting;起播不能作为缓冲成功。保持 AUDIO-BUFFER-01 和任务05开放。
`media-gate-native.spec.js` 新增可选字节参数和放行前独立请求快照,并在恢复失败时
仍保存阶段证据。默认参数、观察时限和 `audio-buffering.spec.js` 均未改变。
明确比较原生时钟、readyState、buffered 与实际已发送字节,不只看最终请求的
sent(放行后该数值已变成完整正文)。复跑参数和原始失败限制见检查点。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,64 @@
# PKG-AUDIO-05 字节前缀与原生缓冲诊断
接续 `bf56d091787bc32c1aacc84ac96904e0349bb0a8`,任务05仍为 doing。
不改播放器/插件、默认媒体、公开 API、事件、分发或依赖,不关闭 AUDIO-BUFFER-01。
## 取证与变化
旧诊断中,Windows WebKit 在 96 KiB 视频 / 32 KiB 音频前缀下不推进,放行后恢复。
本轮仅改变已发字节量,使用相同 `pattern.mp4` 和 `audio-tone.m4a`,原生对照没有
ArtPlayer 实例;早期独立探针连核心脚本也未加载。
- 六项独立起播探针:视频 98,304 字节不推进,188,653 和 251,537 字节推进;
音频 32,768 字节不推进,99,327 和 132,435 字节推进。所有尾部仍扣住,放行后
时钟前进。相同文件的结果不能解释为通用 codec 不支持渐进播放。
- 两项中间前缀探针:视频 131,072、音频 49,152 字节仍不推进/无可信 waiting。
这只是已测点,不推断精确启动阈值、内部缓冲策略或所有系统相同。
- 较大前缀探针增加九秒观察后,视频时钟曾达 9.319 秒而文件只有 8 秒,
ended=true;音频停在 5.611 秒且 readyState=4。均没有可信 waiting。
第一轮视频恢复断言因要求再推进而超时;随后在放行前写检查点并始终保留恢复
错误,没有把媒体结束误报成插件恢复失败,也没有用后来结果覆盖首次异常。
九秒窗口只用于上述一次独立诊断,不进入已提交测试。提交的原生测试保留原先
3 秒推进 / 7 秒 waiting 观察和原恢复断言。新增两个显式诊断变量:
```powershell
$env:ARTPLAYER_MEDIA_GATE_VIDEO_LIMIT = '188653'
$env:ARTPLAYER_MEDIA_GATE_AUDIO_LIMIT = '65536'
yarn test:browser media-gate-native.spec.js --project=webkit --workers=1
```
数值须为正整数且小于文件大小,由真实 HTTP gate 校验;结束诊断后清除这两个变量,
恢复原默认前缀。参数仅用于 native spec,不作用于 `audio-buffering.spec.js`。
在读取原生状态后、`gate.release()` 之前,测试现在拷贝 `requestsBeforeRelease` 并
立即附加 `native-gate-before-release`,避免后续 sent 数字被完整响应改写,也避免
恢复断言失败丢失前半段证据。最终 capability 附件放在 finally 中,包含已到达阶段,
不把缺少 after 当作恢复成功。增加 duration/ended/buffered,便于区分时钟和实际
响应状态。没有改变或删除音轨组合测试的可信 waiting、双时钟恢复与同步断言。
## 验证范围
机器结果见[前缀证据](../baselines/audio-prefix-validation.json)。固定 Node 24.21.0、
Yarn 1.22.22;Playwright Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6,win32。
独立报告/trace 输出保留在 `.cache/audio-prefix-*`,没有覆盖之前的缓冲失败。
较大前缀的四项 WebKit native 流式/完整部分 Range 诊断均正常退出:四项推进、
零项可信 waiting。视频放行前约 7.3 秒,流式音频约 5.6 秒,完整 Range 音频约
7.3 秒,readyState 均为 4;请求快照证明尾部尚未发送。它们只证明观察及放行后
时钟恢复,不是四项缓冲验收通过。
真实 HTTP gate 的三项单测和改动 spec 的只读 lint 通过。默认三引擎诊断的最终
统计及工作区检查在机器证据中记录;诊断通过数必须与 progressed/waiting 分开读。
本轮未重建无变化的生产产物、未重跑插件全矩阵、未验证实体 Safari、声音输出或远端 CI。
上游实现说明可帮助定位:[WebKit Windows 媒体后端](https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/graphics/win/MediaPlayerPrivateMediaFoundation.cpp)
有独立的缓冲事件和时钟处理,
[Playwright 官方说明](https://github.com/microsoft/playwright/blob/main/docs/src/browsers.md)
要求区分各 OS 媒体能力。但本轮没有匹配所装 WebKit 二进制的完整源码和后端执行轨迹,
不能据当前上游文件宣布精确根因已找到。下一步需要对应原生后端或其他实际支持平台
的同输入缓冲证据;不在 ArtPlayer 中加入模拟 waiting、全局延时或放宽测试。
维护入口为 [audio-validation.md](../audio-validation.md) 和
[浏览器说明](../../test/browser/README.md)。本检查点可独立撤销以恢复旧观察方式,
生产运行字节不变;不推送、部署或发布。
+1 -1
View File
@@ -554,7 +554,7 @@
- PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.json) - PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.json)
- PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json) - PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json)
- PKG-AUDIO-04: [记录](changes/2026-09-12-PKG-AUDIO-04-types.md) [记录](baselines/audio-types-validation.json) - PKG-AUDIO-04: [记录](changes/2026-09-12-PKG-AUDIO-04-types.md) [记录](baselines/audio-types-validation.json)
- 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-AUDIO-05: [记录](changes/2026-09-12-PKG-AUDIO-05-combinations.md) [记录](baselines/audio-combinations-first.json) [记录](baselines/audio-combinations-checkpoint.json) [记录](audio-validation.md) [记录](changes/2026-09-15-PKG-AUDIO-05-prefix.md) [记录](baselines/audio-prefix-validation.json)
- 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-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-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) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-first-seek.md) [记录](baselines/auto-thumbnail-first-seek.json) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md) [记录](baselines/auto-thumbnail-native-decoder.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) [记录](changes/2026-09-14-PKG-AUTO-THUMB-03-native-decoder.md) [记录](baselines/auto-thumbnail-native-decoder.json)
+6
View File
@@ -1,5 +1,11 @@
# 进度与证据 # 进度与证据
## PKG-AUDIO-05 同文件前缀与原生缓冲检查点
仅改变同一 MP4/AAC 的 HTTP 放行前缀,Windows WebKit 能起播,但四项较大前缀诊断仍没有可信 waiting。默认三引擎 12 项诊断通过,实际 progressed/waiting 为 Chromium/Firefox 8/8、WebKit 0/4;诊断退出成功不算缓冲验收。新增参数只影响 native spec,放行前请求字节/媒体状态独立保存,恢复失败也保留观察。原插件集成断言、超时和运行字节均未改。见[检查点](changes/2026-09-15-PKG-AUDIO-05-prefix.md)及[证据](baselines/audio-prefix-validation.json)。
AUDIO-BUFFER-01 继续 open、任务05继续 doing;未以原生时钟代替实际画面/声音证据,未定位已安装媒体后端的精确根因。202/266 done、21 doing、43 todo 不变。接续需同输入的实际支持环境/后端证据,同时继续其他可实施任务;未推送或发布。
## REL-04 完成早期回退与主线同步演练 ## REL-04 完成早期回退与主线同步演练
当前 HEAD 隔离重建后,七步连续安装/回退 237 项、五种状态三浏览器实播 15/15、iframe 更名恢复和 Git 冲突同步/中止均通过。旧站点恢复实现未改,本轮复核全部 538 文件、12 条 HTTP 路径和 6 项播放;未重复破坏/恢复,未验证远端部署。发布门槛负例及文件检查 34/34。见[验收](changes/2026-09-15-REL-04-acceptance.md)和[证据](baselines/rollback-acceptance-validation.json)。 当前 HEAD 隔离重建后,七步连续安装/回退 237 项、五种状态三浏览器实播 15/15、iframe 更名恢复和 Git 冲突同步/中止均通过。旧站点恢复实现未改,本轮复核全部 538 文件、12 条 HTTP 路径和 6 项播放;未重复破坏/恢复,未验证远端部署。发布门槛负例及文件检查 34/34。见[验收](changes/2026-09-15-REL-04-acceptance.md)和[证据](baselines/rollback-acceptance-validation.json)。
+4 -2
View File
@@ -2976,11 +2976,13 @@
"refactor/baselines/audio-combinations-checkpoint.json", "refactor/baselines/audio-combinations-checkpoint.json",
"refactor/audio-validation.md", "refactor/audio-validation.md",
"refactor/changes/2026-09-15-CI-01-subtitles-installed.md", "refactor/changes/2026-09-15-CI-01-subtitles-installed.md",
"refactor/baselines/ci-subtitles-installed-validation.json" "refactor/baselines/ci-subtitles-installed-validation.json",
"refactor/changes/2026-09-15-PKG-AUDIO-05-prefix.md",
"refactor/baselines/audio-prefix-validation.json"
], ],
"compatibleResolution": "保留真实 waiting 断言;使用独立原生对照和其他实际支持环境/输入继续验证,不将没有播放推进的诊断当成缓冲通过,也不凭此改变插件 API 或否定真实 Safari 支持。", "compatibleResolution": "保留真实 waiting 断言;使用独立原生对照和其他实际支持环境/输入继续验证,不将没有播放推进的诊断当成缓冲通过,也不凭此改变插件 API 或否定真实 Safari 支持。",
"closureCriteria": "受影响的缓冲/恢复得到真实媒体证据;与原生能力和测试夹具差异分开,发布环境的剩余范围经显式处理。", "closureCriteria": "受影响的缓冲/恢复得到真实媒体证据;与原生能力和测试夹具差异分开,发布环境的剩余范围经显式处理。",
"workspaceState": "CI-01 九包安装扩展的首轮 336 项中,Windows WebKit 八项新旧音频受限媒体组合继续失败。七项未观察到目标可信 waiting,一项受限视频路径耗尽整体时间并在最终状态读取中报页面关闭。其他引擎均通过;保留原始失败,不以销毁后状态反推初始解码错误,不推断新的共同底层原因。" "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."
}, },
{ {
"id": "CHAPTER-LAYOUT-01", "id": "CHAPTER-LAYOUT-01",
+3 -1
View File
@@ -1744,7 +1744,9 @@
"changes/2026-09-12-PKG-AUDIO-05-combinations.md", "changes/2026-09-12-PKG-AUDIO-05-combinations.md",
"baselines/audio-combinations-first.json", "baselines/audio-combinations-first.json",
"baselines/audio-combinations-checkpoint.json", "baselines/audio-combinations-checkpoint.json",
"audio-validation.md" "audio-validation.md",
"changes/2026-09-15-PKG-AUDIO-05-prefix.md",
"baselines/audio-prefix-validation.json"
] ]
}, },
{ {
+11
View File
@@ -230,6 +230,17 @@ playback after release; inspect its progressed/waiting flags before claiming buf
These capability observation timeouts do not relax the integration assertions. These capability observation timeouts do not relax the integration assertions.
Archive the report and results (including traces) before starting another browser suite. Archive the report and results (including traces) before starting another browser suite.
For byte-prefix diagnostics, set `ARTPLAYER_MEDIA_GATE_VIDEO_LIMIT` and/or
`ARTPLAYER_MEDIA_GATE_AUDIO_LIMIT` to an integer byte count strictly between zero
and the corresponding file size. Unset them for the unchanged 96 KiB video / 32 KiB
audio defaults. These variables only affect the native diagnostic, never the audio
plugin integration test. `native-gate-before-release` retains the media state and
an immutable request snapshot before releasing the tail; `native-gate-capability`
also survives failures after that observation and records any successful recovery.
Check `progressed` and `waiting` independently. Larger prefixes can start Windows
WebKit playback without yielding a trusted waiting event; this does not prove the
integration buffering contract. See the audio prefix checkpoint in refactor.
Danmuku input, scheduler, resources and heatmap-density specs accept Danmuku input, scheduler, resources and heatmap-density specs accept
`ARTPLAYER_DANMUKU_ARTIFACT` for a built main/legacy file; unset it to compile the `ARTPLAYER_DANMUKU_ARTIFACT` for a built main/legacy file; unset it to compile the
current JS/TS entry. Run each format sequentially and wait for the command's final current JS/TS entry. Run each format sequentially and wait for the command's final
+12 -7
View File
@@ -1,4 +1,5 @@
import fs from 'node:fs' import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs' import { hash } from '../../refactor/scripts/releases.mjs'
import { createMediaGate } from '../helpers/media-gate.js' import { createMediaGate } from '../helpers/media-gate.js'
import { expect, test } from './fixtures.js' import { expect, test } from './fixtures.js'
@@ -8,8 +9,10 @@ for (const owner of ['video', 'audio']) {
test(`native ${owner} ${completeRanges ? 'complete ranges' : 'streamed response'} capability without ArtPlayer instances`, async ({ page }, testInfo) => { test(`native ${owner} ${completeRanges ? 'complete ranges' : 'streamed response'} capability without ArtPlayer instances`, 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 configuredLimit = process.env[`ARTPLAYER_MEDIA_GATE_${owner.toUpperCase()}_LIMIT`]
const limit = configuredLimit === undefined ? (owner === 'video' ? 96 * 1024 : 32 * 1024) : Number(configuredLimit)
const gate = await createMediaGate(bytes, owner === 'video' ? 'video/mp4' : 'audio/mp4', limit, completeRanges) const gate = await createMediaGate(bytes, owner === 'video' ? 'video/mp4' : 'audio/mp4', limit, completeRanges)
const observation = { file, sha256: hash(bytes), bytes: bytes.length, limit, completeRanges, configuredLimit: configuredLimit ?? null, progressed: false, waiting: false }
try { try {
await page.goto('/test/player.html') await page.goto('/test/player.html')
await page.evaluate(({ owner, url }) => { await page.evaluate(({ owner, url }) => {
@@ -25,29 +28,31 @@ for (const owner of ['video', 'audio']) {
}, { owner, url: gate.url }) }, { owner, url: gate.url })
await expect.poll(() => page.evaluate(() => window.nativeMedia.readyState >= 2 || Boolean(window.nativeMedia.error))).toBe(true) await expect.poll(() => page.evaluate(() => window.nativeMedia.readyState >= 2 || Boolean(window.nativeMedia.error))).toBe(true)
await page.locator('#play').click() await page.locator('#play').click()
let progressed = false
let waiting = false
try { try {
await page.waitForFunction(() => window.nativeMedia.currentTime > 0.3, null, { timeout: 3000 }) await page.waitForFunction(() => window.nativeMedia.currentTime > 0.3, null, { timeout: 3000 })
progressed = true observation.progressed = true
await page.waitForFunction(() => window.nativeEvents.some(event => event.event === 'waiting' && event.trusted && event.time > 0.3 && event.readyState < 3), null, { timeout: 7000 }) await page.waitForFunction(() => window.nativeEvents.some(event => event.event === 'waiting' && event.trusted && event.time > 0.3 && event.readyState < 3), null, { timeout: 7000 })
waiting = true observation.waiting = true
} }
catch (error) { catch (error) {
if (error.name !== 'TimeoutError') if (error.name !== 'TimeoutError')
throw error throw error
} }
const before = await page.evaluate(() => ({ time: window.nativeMedia.currentTime, readyState: window.nativeMedia.readyState, paused: window.nativeMedia.paused, error: window.nativeMedia.error?.code || null, instances: window.Artplayer.instances.length, events: window.nativeEvents })) const before = await page.evaluate(() => ({ time: window.nativeMedia.currentTime, duration: window.nativeMedia.duration, ended: window.nativeMedia.ended, readyState: window.nativeMedia.readyState, paused: window.nativeMedia.paused, error: window.nativeMedia.error?.code || null, instances: window.Artplayer.instances.length, events: window.nativeEvents, buffered: Array.from({ length: window.nativeMedia.buffered.length }, (_, index) => [window.nativeMedia.buffered.start(index), window.nativeMedia.buffered.end(index)]) }))
observation.before = before
observation.requestsBeforeRelease = structuredClone(gate.requests)
await testInfo.attach('native-gate-before-release', { contentType: 'application/json', body: JSON.stringify(observation) })
expect(before.instances).toBe(0) expect(before.instances).toBe(0)
expect(gate.blocked).toBeGreaterThan(0) expect(gate.blocked).toBeGreaterThan(0)
gate.release() gate.release()
await page.locator('#play').click() await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.nativeMedia.currentTime)).toBeGreaterThan(before.time + 0.3) await expect.poll(() => page.evaluate(() => window.nativeMedia.currentTime)).toBeGreaterThan(before.time + 0.3)
expect(await page.evaluate(() => window.nativeMedia.error)).toBeNull() expect(await page.evaluate(() => window.nativeMedia.error)).toBeNull()
await testInfo.attach('native-gate-capability', { contentType: 'application/json', body: JSON.stringify({ file, sha256: hash(bytes), limit, completeRanges, progressed, waiting, before, requests: gate.requests, after: await page.evaluate(() => ({ time: window.nativeMedia.currentTime, readyState: window.nativeMedia.readyState, paused: window.nativeMedia.paused })) }) }) observation.after = await page.evaluate(() => ({ time: window.nativeMedia.currentTime, readyState: window.nativeMedia.readyState, paused: window.nativeMedia.paused }))
} }
finally { finally {
try { try {
await testInfo.attach('native-gate-capability', { contentType: 'application/json', body: JSON.stringify({ ...observation, requests: gate.requests }) })
await page.evaluate(() => { await page.evaluate(() => {
window.nativeMedia?.pause() window.nativeMedia?.pause()
window.nativeMedia?.removeAttribute('src') window.nativeMedia?.removeAttribute('src')