test(audio): [PKG-AUDIO-05] checkpoint source-switch regressions

This commit is contained in:
Harvey Zhao committed 2026-09-12 03:06:54 +08:00
1 parent 13c898c149
commit b5e96664ee
11 files changed
+8785 -87

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,24 @@
# PKG-AUDIO-05:同步边界与真实媒体组合
状态:未完成;起点 13c898c1,已核实 Audio-04 独立提交且工作区干净。
覆盖已发布核心和候选核心,与旧/新 audio 的合法组合。先验证主视频连续切源、
暂停后恢复、外部音频多实例隔离,以及正/负偏移和超时长的真实媒体行为,再补真实
网络缓冲/恢复。保留浏览器事件、输入指纹、失败报告和 trace,不能以受控 emit
充当真实 waiting。物理设备和实际声音输出缺口继续明确记录。
Audio-04 的模块边界及旧类型入口不变;发现问题先比较已发布行为并登记兼容差异。
首轮 36 项为 28 通过、8 失败。六项是候选核心(所有三引擎、两种 audio)连续切源
最终视频与音频均停在 0:源码每次重新读取 playing,第二次读到前一次内部 pause
后的 false,原播放意图丢失。已拆 CORE-24 为独立修复与提交,Audio-05 暂回 todo
等待前置;不把核心历史阶段通过视为已覆盖这次新发现。
另有 WebKit 旧核心 + 新 audio 切源后视频继续、音频暂停的失败,AUDIO-RESUME-01
保持 open,继续核实 seeked/canplay 就绪时序。旧核心 + 旧 audio 边界返回 16.01
而断言精确 16,需原生媒体对照,不算候选回归。没有跳过或重试这些失败。
原始报告和 trace 已独立保存,见 [首轮证据](../baselines/audio-combinations-first.json)。
共用输入/事件/清理辅助移入 audio-fixture.js,通过显式 useAudioFixture() 为每个测试
文件注册各自 hooks,避免 import 缓存让后一个 suite 缺输入。原有媒体用例也会重跑。
本阶段只提交明确标注 checkpoint 的测试与发现,未提交生产修复,也未完成 Audio-05。
+4
View File
@@ -4,6 +4,10 @@ CORE-22 汇总核心自有源码、公开声明、生成产物和当前固定消
本文件不能作为全部生态包或 npm 发布放行。实际执行结果、源码/产物指纹和剩余事项见
[交付记录](changes/2026-09-11-CORE-22-core-acceptance.md);任务状态以 tasks.json 为准。
后续 Audio-05 的新组合测试复现了候选核心连续切源丢失播放意图,见
[首轮证据](baselines/audio-combinations-first.json)。CORE-SOURCE-01/CORE-24 作为新发现
继续修复,当前不能据本文件历史阶段结果放行;原有通过记录保留其当时的测试范围。
## 契约与回归入口
| 契约 | 本阶段检查与可重跑入口 | 明确边界 |
+7 -4
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 216 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。
状态:todo 146 / doing 1 / blocked 0 / done 69 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 147 / doing 1 / blocked 0 / done 69 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -14,7 +14,7 @@
| 包 | 基线版本 | 任务 |
| --- | --- | --- |
| artplayer | 5.4.1 | CORE-01, CORE-02, CORE-03, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, CORE-11, CORE-12, CORE-13, CORE-14, CORE-15, CORE-16, CORE-17, CORE-18, CORE-19, CORE-20, CORE-21, CORE-23, CORE-22 |
| artplayer | 5.4.1 | CORE-01, CORE-02, CORE-03, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, CORE-11, CORE-12, CORE-13, CORE-14, CORE-15, CORE-16, CORE-17, CORE-18, CORE-19, CORE-20, CORE-21, CORE-23, CORE-22, CORE-24 |
| 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-05, PKG-AMBILIGHT-06 |
| artplayer-plugin-asr | 2.1.0 | PKG-ASR-01, PKG-ASR-02, PKG-ASR-03, PKG-ASR-04, PKG-ASR-05, PKG-ASR-06 |
@@ -128,6 +128,7 @@
| CORE-21 | artplayer<br>生成核心声明并校验包内容 | CORE-20, ENG-07 | 由 TS 生成的既有入口声明与三种发布产物 | 内部类型不泄漏,旧 JS/TS 消费者与 i18n 通过;逐项完成 CORE-07 留下的 BASE-TYPE-04/05/07 声明方案,旧合法消费者与实际返回分别留证;不能以虚假的交叉返回类型或改变运行时来掩盖冲突,未决项阻止核心声明出口;处理 CORE-10 / BASE-TYPE-08 的 setter-only 属性虚构 getter 声明,并复核 PlaybackControls 精确视图与旧 toggle 声明共存策略 | H | done |
| CORE-23 | artplayer<br>检查并补齐键盘、焦点与可访问名称 | CORE-13, CORE-14, CORE-17, ENG-05, BASE-04 | 主要控件/设置/模式退出的键盘与焦点回归、名称和字幕可用性检查及必要兼容修正 | 保持旧快捷键和 DOM/CSS 钩子;真实浏览器验证,不以静态属性检查代替交互;处理 BASE-DOM-01 的主要控件 Tab 不可达,保留既有名称与快捷键 | H | done |
| CORE-22 | artplayer<br>核心阶段完整验收 | CORE-21, CORE-23, PILOT-01, ENG-08, ENG-10, ENG-11 | 核心与旧插件的可自动化完整回归、资源/性能及明确外部验证缺口 | 核心自动化和公开差异处置通过;真实环境缺口链接包集成/REL 门槛,阶段完成不代表可公开发布 | H | done |
| CORE-24 | artplayer<br>修复连续切源的播放意图继承 | CORE-22, PKG-AUDIO-04 | 连续切源保留原播放意图,同时尊重显式暂停与最新来源,回归旧/新 audio | 真实暂停状态的受控测试、重入/取消/失败/销毁、三引擎和 main/legacy 产物验证通过;不恢复过期来源的公开副作用 | H | todo |
## 5 包迁移:artplayer-plugin-chapter
@@ -159,7 +160,7 @@
| 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 | 主视频切源和缓冲恢复,多实例独立音频、旧核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-AUDIO-05 | artplayer-plugin-audio-track<br>验证新旧核心和组合 | PKG-AUDIO-04, CORE-22, CORE-24 | 主视频切源和缓冲恢复,多实例独立音频、旧核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-AUDIO-06 | artplayer-plugin-audio-track<br>验证分发并同步文档 | PKG-AUDIO-05, ENG-07 | audio.track.js 示例与运行返回值一致的声明 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
## 5 包迁移:artplayer-plugin-auto-thumbnail
@@ -462,6 +463,7 @@
- CORE-21: [记录](changes/2026-09-11-CORE-21-public-declarations.md) [记录](baselines/core-declarations-validation.json) [记录](core-public-types.md) [记录](typechecking.md)
- CORE-23: [记录](changes/2026-09-11-CORE-23-keyboard-focus.md) [记录](baselines/keyboard-validation.json)
- CORE-22: [记录](changes/2026-09-11-CORE-22-core-acceptance.md) [记录](baselines/core-setting-navigation-partial.json) [记录](core-acceptance.md) [记录](baselines/core-acceptance-validation.json)
- CORE-24: [记录](baselines/audio-combinations-first.json)
- PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md)
- PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json)
- PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json)
@@ -470,6 +472,7 @@
- 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-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)
- PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md)
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
+13
View File
@@ -1,5 +1,18 @@
# 进度与证据
## 当前实施:Audio-05 发现连续切源回归,拆分 CORE-24
Audio-04 已独立提交 13c898c1。新组合测试覆盖旧/新核心、旧/新 audio 的连续切源、
双实例和偏移边界,首轮 28 通过、8 失败,原始报告/trace 和输入已保存。
六项候选核心切源失败确认原播放意图被前一次内部暂停覆盖;新增 CORE-24 独立修复,
Audio-05 暂回 todo 等待前置。另有 WebKit 旧核心/新音频恢复问题待精确诊断,以及
旧版边界 16.01 与精确 16 断言差异需要原生对照。没有修改测试使它们假通过。
任务现为 217 项、69 done、1 doing、147 todo。下一步 CORE-24 的播放意图与取消测试,
完成后继续 Audio-05 的恢复、真实缓冲和其余边界。HLS 仍 doing,全局目标不变。
见 [进行中记录](changes/2026-09-12-PKG-AUDIO-05-combinations.md) 和
[首轮证据](baselines/audio-combinations-first.json)。本次为测试发现检查点,不是完成提交。
## 最新完成:PKG-AUDIO-04 严格 TS 与兼容类型入口
Audio 两个职责模块已迁移 TS,累计 256 个生产 TS。默认/legacy 保留原 update 类型
+2
View File
@@ -130,3 +130,5 @@
| AUDIO-SYNC-01 | open / 已复现 | Audio Track 未同步原生暂停/结束,偏移边界与切源/缓冲仍待完整验证 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 |
| AUDIO-TYPE-01 | accepted-with-scope / 源码/产物事实 | Audio Track update 部分配置与公开/编辑器声明不一致 | PKG-AUDIO-04 |
| AUDIO-DEMO-01 | open / 源码/产物事实 | Audio Track README 示例链接名称不匹配且缺维护说明 | PKG-AUDIO-06 |
| CORE-SOURCE-01 | open / 已复现 | 候选核心连续切源丢失原播放意图 | CORE-24 |
| AUDIO-RESUME-01 | open / 已复现 | WebKit 旧核心切源后候选音频保持暂停 | PKG-AUDIO-05 |
+29
View File
@@ -2726,6 +2726,35 @@
],
"compatibleResolution": "修正 audio.track 示例路径,补真实模块地图及 update/生命周期说明。",
"closureCriteria": "README、示例、公开声明一致,8082 正式 demo 和包分发验证通过。"
},
{
"id": "CORE-SOURCE-01",
"title": "候选核心连续切源丢失原播放意图",
"confirmation": "reproduced",
"status": "open",
"owners": [
"CORE-24"
],
"evidence": [
"refactor/baselines/audio-combinations-first.json",
"packages/artplayer/src/source/switch.ts"
],
"compatibleResolution": "继承被替代切源操作的播放意图;显式暂停可以取消恢复,过期操作继续只结算不发布副作用。",
"closureCriteria": "新旧对照的连续切源、手动暂停、重入/取消/错误/销毁和正式产物三引擎验证通过。"
},
{
"id": "AUDIO-RESUME-01",
"title": "WebKit 旧核心切源后候选音频保持暂停",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-AUDIO-05"
],
"evidence": [
"refactor/baselines/audio-combinations-first.json"
],
"compatibleResolution": "先核实 seeked/canplay 和宿主 playing 的事件顺序;修复真实可播放后音频缺少恢复入口,保持旧核心支持。",
"closureCriteria": "保留原始失败,精确事件复现并有旧核心/新核心三引擎回归;不可仅重复到绿灯。"
}
]
}
+25 -2
View File
@@ -1238,6 +1238,25 @@
"baselines/core-acceptance-validation.json"
]
},
{
"id": "CORE-24",
"phase": "3-4 核心迁移",
"title": "修复连续切源的播放意图继承",
"scope": [
"artplayer"
],
"dependsOn": [
"CORE-22",
"PKG-AUDIO-04"
],
"status": "todo",
"risk": "H",
"deliverable": "连续切源保留原播放意图,同时尊重显式暂停与最新来源,回归旧/新 audio",
"acceptance": "真实暂停状态的受控测试、重入/取消/失败/销毁、三引擎和 main/legacy 产物验证通过;不恢复过期来源的公开副作用",
"evidence": [
"baselines/audio-combinations-first.json"
]
},
{
"id": "PKG-CHAPTER-01",
"phase": "5 包迁移:artplayer-plugin-chapter",
@@ -1551,13 +1570,17 @@
],
"dependsOn": [
"PKG-AUDIO-04",
"CORE-22"
"CORE-22",
"CORE-24"
],
"status": "todo",
"risk": "H",
"deliverable": "主视频切源和缓冲恢复,多实例独立音频、旧核心",
"acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-AUDIO-05-combinations.md",
"baselines/audio-combinations-first.json"
]
},
{
"id": "PKG-AUDIO-06",
+112
View File
@@ -0,0 +1,112 @@
import { useAudioFixture } from './audio-fixture.js'
import { expect, test } from './fixtures.js'
const { openAudio } = useAudioFixture()
for (const core of ['published', 'candidate']) {
for (const plugin of ['published', 'source']) {
test(`${core} core + ${plugin} audio: rapid main/audio switches settle on the last pair`, async ({ page }, testInfo) => {
await openAudio(page, core, plugin, testInfo)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(0.3)
await page.evaluate(async () => {
const { art, audioPlugin } = window
window.originalAudio = audioPlugin.audio
const pending = []
for (const source of ['first', 'second', 'last']) {
pending.push(art.switchUrl(`/test/pattern.mp4?pair=${source}`))
audioPlugin.update({ url: `/test/audio-tone.m4a?pair=${source}` })
}
await Promise.all(pending)
})
await expect.poll(() => page.evaluate(() => window.art.video.currentSrc)).toContain('pair=last')
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentSrc)).toContain('pair=last')
await expect.poll(() => page.evaluate(() => !window.art.video.paused && !window.audioPlugin.audio.paused && window.art.currentTime > 0.3)).toBe(true)
await expect.poll(() => page.evaluate(() => Math.abs(window.art.currentTime - window.audioPlugin.audio.currentTime))).toBeLessThan(0.5)
expect(await page.evaluate(() => window.originalAudio === window.audioPlugin.audio)).toBe(true)
await page.evaluate(async () => {
window.art.pause()
await window.art.switchUrl('/test/pattern.mp4?pair=paused')
window.audioPlugin.update({ url: '/test/audio-tone.m4a?pair=paused' })
})
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBeGreaterThanOrEqual(2)
expect(await page.evaluate(() => window.art.video.paused && window.audioPlugin.audio.paused)).toBe(true)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(0.3)
expect(await page.evaluate(() => [window.art.video.error, window.audioPlugin.audio.error])).toEqual([null, null])
await page.evaluate(() => window.art.destroy())
})
test(`${core} core + ${plugin} audio: two active players keep clocks and ownership independent`, async ({ page }, testInfo) => {
await openAudio(page, core, plugin, testInfo)
await page.evaluate(() => {
window.art.option.mutex = false
const container = document.createElement('div')
container.style.cssText = 'width:320px;height:180px'
document.body.append(container)
window.otherArt = new window.Artplayer({ container, url: '/test/pattern.mp4?instance=other', mutex: false, muted: true, plugins: [window.artplayerPluginAudioTrack({ url: '/test/audio-tone.m4a?instance=other', offset: 0.5, sync: 0.1 })] })
window.otherAudio = window.otherArt.plugins.artplayerPluginAudioTrack.audio
const play = document.querySelector('#play')
play.onclick = () => Promise.all([window.art.play(), window.otherArt.play()])
})
await expect.poll(() => page.evaluate(() => window.otherArt.isReady && window.otherAudio.readyState >= 2)).toBe(true)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => !window.audioPlugin.audio.paused && !window.otherAudio.paused && window.art.currentTime > 0.2 && window.otherArt.currentTime > 0.2)).toBe(true)
await page.evaluate(() => {
window.art.pause()
window.art.seek = 3
window.art.volume = 0.2
window.art.playbackRate = 1.5
window.otherArt.volume = 0.7
})
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeCloseTo(3, 1)
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.playbackRate)).toBe(1.5)
await expect.poll(() => page.evaluate(() => window.otherAudio.volume)).toBeCloseTo(0.7)
expect(await page.evaluate(() => window.otherAudio.playbackRate)).toBe(1)
expect(await page.evaluate(() => window.otherAudio !== window.audioPlugin.audio)).toBe(true)
await expect.poll(() => page.evaluate(() => Math.abs(window.otherAudio.currentTime - window.otherArt.currentTime - 0.5))).toBeLessThan(0.5)
const before = await page.evaluate(() => {
window.art.destroy()
return window.otherAudio.currentTime
})
await expect.poll(() => page.evaluate(() => window.otherAudio.currentTime)).toBeGreaterThan(before + 0.3)
expect(await page.evaluate(() => window.audioPlugin.audio.paused && !window.otherAudio.paused)).toBe(true)
expect(await page.evaluate(() => window.otherAudio.currentSrc)).toContain('instance=other')
expect(await page.evaluate(() => window.otherAudio.error)).toBeNull()
await testInfo.attach('independent-audio', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ left: { time: window.audioPlugin.audio.currentTime, paused: window.audioPlugin.audio.paused, rate: window.audioPlugin.audio.playbackRate }, right: { time: window.otherAudio.currentTime, videoTime: window.otherArt.currentTime, rate: window.otherAudio.playbackRate, paused: window.otherAudio.paused } }))) })
await page.evaluate(() => window.otherArt.destroy())
expect(await page.evaluate(() => window.otherAudio.paused)).toBe(true)
})
test(`${core} core + ${plugin} audio: native offset bounds recover to an in-range seek`, async ({ page }, testInfo) => {
await openAudio(page, core, plugin, testInfo)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(0.2)
await page.evaluate(() => {
window.art.pause()
window.audioPlugin.update({ offset: -2 })
window.art.seek = 0.5
})
await expect.poll(() => page.evaluate(() => !window.art.video.seeking && !window.audioPlugin.audio.seeking)).toBe(true)
const negative = await page.evaluate(() => ({ time: window.audioPlugin.audio.currentTime, error: window.audioPlugin.audio.error?.code || null }))
expect(negative).toEqual({ time: 0, error: null })
await page.evaluate(() => {
window.audioPlugin.update({ offset: 20 })
window.art.seek = 1
})
await expect.poll(() => page.evaluate(() => !window.art.video.seeking && !window.audioPlugin.audio.seeking)).toBe(true)
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBe(16)
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
await page.evaluate(() => {
window.audioPlugin.update({ offset: -0.5 })
window.art.seek = 3
})
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeCloseTo(2.5, 1)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(2.8)
await expect.poll(() => page.evaluate(() => Math.abs(window.audioPlugin.audio.currentTime - window.art.currentTime + 0.5))).toBeLessThan(0.5)
await testInfo.attach('offset-boundary', { contentType: 'application/json', body: JSON.stringify({ negative, overflow: 16, recovered: true }) })
await page.evaluate(() => window.art.destroy())
})
}
}
+87
View File
@@ -0,0 +1,87 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
export function useAudioFixture() {
let publishedCode
let sourceCode
let evidence
const samples = 16000 * 16
const tone = fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))
const toneManifest = JSON.parse(fs.readFileSync(new URL('./media/audio-tone.json', import.meta.url)))
const wav = Buffer.alloc(44 + samples * 2)
wav.write('RIFF', 0)
wav.writeUInt32LE(wav.length - 8, 4)
wav.write('WAVEfmt ', 8)
wav.writeUInt32LE(16, 16)
wav.writeUInt16LE(1, 20)
wav.writeUInt16LE(1, 22)
wav.writeUInt32LE(16000, 24)
wav.writeUInt32LE(32000, 28)
wav.writeUInt16LE(2, 32)
wav.writeUInt16LE(16, 34)
wav.write('data', 36)
wav.writeUInt32LE(samples * 2, 40)
for (let index = 0; index < samples; index++)
wav.writeInt16LE(index % 80 < 40 ? 2000 : -2000, 44 + index * 2)
test.beforeAll(async () => {
assert.equal(hash(tone), toneManifest.sha256)
assert.equal(tone.length, toneManifest.bytes)
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Audio source tests cannot represent installed artifacts')
const { release } = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/audio-track-release.json', import.meta.url)))
const member = 'package/dist/artplayer-plugin-audio-track.js'
const bytes = readMember(await ensureArchive(release), member)
assert.equal(hash(bytes), release.files[member])
publishedCode = bytes.toString()
sourceCode = process.env.ARTPLAYER_AUDIO_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_AUDIO_ARTIFACT, 'utf8') : await compilePackage(release.name, 'umd')
evidence = { release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_AUDIO_ARTIFACT || 'workspace source build', audio: { sha256: hash(tone), bytes: tone.length, format: 'AAC mono 48000 Hz, 16 seconds, 440 Hz tone in MP4' }, wav: { sha256: hash(wav), bytes: wav.length, format: 'PCM16LE mono 16000 Hz, 16 seconds, integer square wave 200 Hz' }, scope: 'Source or explicit artifact browser checks; no physical device or installed package acceptance' }
})
test.afterEach(async ({ page }, testInfo) => {
const state = await page.evaluate(() => {
const audio = window.audioPlugin?.audio
const video = window.art?.video
const media = element => element && ({ src: element.currentSrc, attribute: element.getAttribute('src'), time: element.currentTime, paused: element.paused, readyState: element.readyState, networkState: element.networkState, error: element.error?.code, rate: element.playbackRate, seekable: Array.from({ length: element.seekable.length }, (_, index) => [element.seekable.start(index), element.seekable.end(index)]) })
return { audio: media(audio), video: media(video), events: window.audioEvents, hostEvents: window.audioHostEvents, warnings: window.audioWarnings }
}).catch(error => ({ error: error.message }))
await testInfo.attach('audio-state', { contentType: 'application/json', body: JSON.stringify(state) })
})
async function openAudio(page, core, plugin, testInfo, url = '/test/audio-tone.m4a?source=first') {
await testInfo.attach('audio-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
await page.evaluate((url) => {
window.audioEvents = []
window.audioHostEvents = []
window.audioWarnings = []
const warn = console.warn.bind(console)
console.warn = (...args) => {
window.audioWarnings.push(args.map(String))
warn(...args)
}
window.art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4',
muted: true,
plugins: [window.artplayerPluginAudioTrack({ url, sync: 0.1 })],
})
window.audioPlugin = window.art.plugins.artplayerPluginAudioTrack
for (const event of ['play', 'pause', 'seek', 'video:waiting', 'video:playing', 'video:seeking', 'video:seeked', 'video:pause', 'video:ended'])
window.art.on(event, () => window.audioHostEvents.push({ event, videoTime: window.art.currentTime, audioTime: window.audioPlugin.audio.currentTime, audioPaused: window.audioPlugin.audio.paused }))
for (const event of ['loadedmetadata', 'playing', 'pause', 'seeked', 'error', 'emptied'])
window.audioPlugin.audio.addEventListener(event, () => window.audioEvents.push({ event, time: window.audioPlugin.audio.currentTime, src: window.audioPlugin.audio.currentSrc }))
document.querySelector('#play').onclick = () => window.art.play()
}, url)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
if (url)
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBeGreaterThanOrEqual(2)
}
return { openAudio, wav }
}
+2 -81
View File
@@ -1,86 +1,7 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { useAudioFixture } from './audio-fixture.js'
import { expect, test } from './fixtures.js'
let publishedCode
let sourceCode
let evidence
const samples = 16000 * 16
const tone = fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))
const toneManifest = JSON.parse(fs.readFileSync(new URL('./media/audio-tone.json', import.meta.url)))
const wav = Buffer.alloc(44 + samples * 2)
wav.write('RIFF', 0)
wav.writeUInt32LE(wav.length - 8, 4)
wav.write('WAVEfmt ', 8)
wav.writeUInt32LE(16, 16)
wav.writeUInt16LE(1, 20)
wav.writeUInt16LE(1, 22)
wav.writeUInt32LE(16000, 24)
wav.writeUInt32LE(32000, 28)
wav.writeUInt16LE(2, 32)
wav.writeUInt16LE(16, 34)
wav.write('data', 36)
wav.writeUInt32LE(samples * 2, 40)
for (let index = 0; index < samples; index++)
wav.writeInt16LE(index % 80 < 40 ? 2000 : -2000, 44 + index * 2)
test.beforeAll(async () => {
assert.equal(hash(tone), toneManifest.sha256)
assert.equal(tone.length, toneManifest.bytes)
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Audio source tests cannot represent installed artifacts')
const { release } = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/audio-track-release.json', import.meta.url)))
const member = 'package/dist/artplayer-plugin-audio-track.js'
const bytes = readMember(await ensureArchive(release), member)
assert.equal(hash(bytes), release.files[member])
publishedCode = bytes.toString()
sourceCode = process.env.ARTPLAYER_AUDIO_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_AUDIO_ARTIFACT, 'utf8') : await compilePackage(release.name, 'umd')
evidence = { release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_AUDIO_ARTIFACT || 'workspace source build', audio: { sha256: hash(tone), bytes: tone.length, format: 'AAC mono 48000 Hz, 16 seconds, 440 Hz tone in MP4' }, wav: { sha256: hash(wav), bytes: wav.length, format: 'PCM16LE mono 16000 Hz, 16 seconds, integer square wave 200 Hz' }, scope: 'Source or explicit artifact browser checks; no physical device or installed package acceptance' }
})
test.afterEach(async ({ page }, testInfo) => {
const state = await page.evaluate(() => {
const audio = window.audioPlugin?.audio
const video = window.art?.video
const media = element => element && ({ src: element.currentSrc, attribute: element.getAttribute('src'), time: element.currentTime, paused: element.paused, readyState: element.readyState, networkState: element.networkState, error: element.error?.code, rate: element.playbackRate, seekable: Array.from({ length: element.seekable.length }, (_, index) => [element.seekable.start(index), element.seekable.end(index)]) })
return { audio: media(audio), video: media(video), events: window.audioEvents, hostEvents: window.audioHostEvents, warnings: window.audioWarnings }
}).catch(error => ({ error: error.message }))
await testInfo.attach('audio-state', { contentType: 'application/json', body: JSON.stringify(state) })
})
async function openAudio(page, core, plugin, testInfo, url = '/test/audio-tone.m4a?source=first') {
await testInfo.attach('audio-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
await page.evaluate((url) => {
window.audioEvents = []
window.audioHostEvents = []
window.audioWarnings = []
const warn = console.warn.bind(console)
console.warn = (...args) => {
window.audioWarnings.push(args.map(String))
warn(...args)
}
window.art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4',
muted: true,
plugins: [window.artplayerPluginAudioTrack({ url, sync: 0.1 })],
})
window.audioPlugin = window.art.plugins.artplayerPluginAudioTrack
for (const event of ['play', 'pause', 'seek', 'video:waiting', 'video:playing', 'video:seeking', 'video:seeked', 'video:pause', 'video:ended'])
window.art.on(event, () => window.audioHostEvents.push({ event, videoTime: window.art.currentTime, audioTime: window.audioPlugin.audio.currentTime, audioPaused: window.audioPlugin.audio.paused }))
for (const event of ['loadedmetadata', 'playing', 'pause', 'seeked', 'error', 'emptied'])
window.audioPlugin.audio.addEventListener(event, () => window.audioEvents.push({ event, time: window.audioPlugin.audio.currentTime, src: window.audioPlugin.audio.currentSrc }))
document.querySelector('#play').onclick = () => window.art.play()
}, url)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
if (url)
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBeGreaterThanOrEqual(2)
}
const { openAudio, wav } = useAudioFixture()
async function playToEnd(page) {
await page.evaluate(() => {