test(audio): [PKG-AUDIO-02] cover released behavior and real media

This commit is contained in:
Harvey Zhao committed 2026-09-12 02:08:57 +08:00
1 parent 8c0a2feed3
commit 803e87252e
18 files changed
+8519 -13

No files matched your search

+1
View File
@@ -3,4 +3,5 @@
test/browser/media/hls/*.m3u8 text eol=lf test/browser/media/hls/*.m3u8 text eol=lf
test/browser/fixtures/hls-grouped.m3u8 text eol=lf test/browser/fixtures/hls-grouped.m3u8 text eol=lf
test/browser/media/hls/*.mpegts binary test/browser/media/hls/*.mpegts binary
test/browser/media/*.m4a binary
refactor/baselines/core-vendor/*.txt text eol=lf refactor/baselines/core-vendor/*.txt text eol=lf
+1 -1
View File
@@ -50,7 +50,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports", "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs", "typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/hls-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", "test:unit": "node --test test/playback.test.js test/dash-control.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs", "test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline", "test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test", "test:browser": "playwright test",
+61
View File
@@ -0,0 +1,61 @@
# Audio Track 行为与媒体测试
PKG-AUDIO-02;公开来源见 [1.1.0 契约](baselines/audio-track-contract.md),
固定执行结果及失败保留见 [验证记录](baselines/audio-validation.json)。
## 已实现命令
```sh
node --test test/audio-track.test.js
yarn ci:check
yarn test:browser test/browser/audio-track.spec.js --trace on
```
Node 专项 38 项:源码和真实发布 main/legacy/module,初始化/延迟安装、暴露 audio、
严格阈值、正负偏移、seek/播放前同步、playing 条件、waiting/pause、音量/静音/倍率、
同/空/新 URL、三处 play 拒绝、销毁和多实例隔离。Audio 是记录调用的受控对象,不模拟解码。
工厂输入跨 realm 检查使用冻结归档,源码使用既有 JS/TS loader,可随迁移继续回归。
真实浏览器最终 36 项通过,无重试、跳过:每引擎 8 项新旧核心/插件播放及失败恢复,
1 项原生 WAV 诊断、1 项旧插件空 src 销毁问题、2 项不同核心的原生暂停/结束缺陷观察。
因此 36 通过不代表 36 项无缺陷的候选功能验收:9 项复现历史问题,3 项是原生能力诊断。
旧核心固定 5.4.0、旧插件固定 1.1.0;候选来自源码构建,非隔离安装包。
播放证据包括主视频实际解码颜色、媒体时钟、AAC 音频加载/时钟、偏移 seek、倍率/音量、
音频切源后的同一元素及同步、503 加载失败后换源恢复。公开 audio 的事件、状态、
原始 warning、代码/媒体/核心产物指纹随测试附件保存。不声称已经测得扬声器输出或音画主观同步。
Node 控制 waiting/playing 与 play 拒绝;实际浏览器网络缓冲/自动播放策略和移动环境由 05 补充。
## 样本与失败归因
test/browser/media/audio-tone.m4a 是本地生成的 16 秒 440 Hz 单声道 AAC。
生成命令、FFmpeg 版本、ffprobe 及哈希见旁边 audio-tone.json,测试重验哈希。
服务器沿用已有 GET/HEAD/Range,增加 audio/mp4 MIME;无外网音频依赖。
WAV 诊断使用整数方波,完整参数/哈希冻结。直接原生 Audio(未创建 ArtPlayer)在
Windows WebKit 也给出错误 4;AAC 矩阵通过不能覆盖该 WAV 或真实 Safari 的能力。
初轮 24 失败及后续诊断均保存,没有覆盖成绿灯:
1. 测试先用了 currentTime setter;它不发 seek 事件。修正为已有公开 art.seek,未修改核心。
2. 旧插件 src='' 后 readyState=0、paused=true,但 currentSrc 可能保留且 error=4。
正常基线清理检查属性/暂停/就绪状态,另立发布版缺陷断言,不把旧错误当作已修复。
3. WAV 在 Windows WebKit 失败;用无播放器原生 Audio 对照隔离,并增加独立 AAC 测试输入。
4. 最初 AAC route 固定返回 200 全文件,seek 返回 0;改用现有 Range 服务器,验证 seekable
覆盖目标再 seek。没有增加超时或移除实际 seek 断言。
5. Windows WebKit 视频尺寸可能反映布局,既有 browser/README.md 已说明;播放证据改为
固定样本的真实解码像素,保留已知尺寸限制,未改媒体实现。
最终源码/夹具都无生产行为变动。一次完整 CI 577 项(534+11+32)、254 个生产 TS 通过,
最终新增的浏览器观察经定向 lint 和三引擎验证。所有失败报告/trace 均复制到独立 cache 目录,
冻结 JSON 保存摘要、输入和关键状态;cache 丢失时应重跑,不能称旧 trace 仍存在。
## 后续修复与交接
- AUDIO-LIFE-01 已由旧版受控/实际媒体复现:保留的 update 可复活 src;src='' 触发新媒体错误。
03 应使用关闭守卫、插件自身订阅清理和无错误的媒体释放;保留同一公开 audio 及同步 update。
- AUDIO-SYNC-01 部分已复现:主视频直接 pause 或播放结束,外部音频仍播放;03 修复原生事件同步。
负偏移起点/时长边界、真实缓冲和快速切源仍须在 03/05 接续,不能以本次通过关闭全部条目。
- AUDIO-TYPE-01/DEMO-01 继续由 04/06 处理。归档分发、Monaco/8082 与设备验收尚未完成。
浏览器测试均共用端口 8084 和输出目录;不要同时执行多个 suite。开始下一次前复制报告
和结果目录(包含 trace),并记录对应测试输入。Node CI 可独立并行执行。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,43 @@
# PKG-AUDIO-02:外部音频行为基线
状态:本步测试基线验收完成;起点 8c0a2fee。遵循已冻结的 Audio Track 1.1.0 契约。
范围是源码及真实发布三格式的可控行为、错误与生命周期测试,以及真实浏览器的
外部音频/主视频播放、seek、切源和清理。受控 Audio 不被描述为真实解码证据。
公开行为不在本步修改;复现的历史问题交由 03 修复、04 类型迁移。
## 交付与验证
新增 test/audio-track.test.js 和窄范围受控 Audio helper,源码及发布三格式共 38 项通过。
公开 audio、工厂延迟安装、默认阈值、偏移正负、配置更新、事件、Promise 拒绝及清理均覆盖。
Node 测试加入现有 test:unit 脚本,不新增依赖或 lock 改动。
新增 audio-track.spec.js 和自行生成的 AAC 媒体/来源 manifest,服务器复用 Range 处理。
三引擎最终 36 项通过(其中 9 项历史缺陷复现、3 项原生能力诊断),没有重试/跳过。
音频与视频时钟、真实解码像素、偏移 seek、音量倍率、换音源/错误恢复、清理状态均验证。
577 项完整 CI、254 生产 TS 通过。最终浏览器补充又通过定向 lint 与三引擎执行。
计划/风险生成校验和 Git diff 检查通过:216 项任务、126 项风险,未豁免未解决项。
原始失败及下一步见 [测试说明](../audio-validation.md),所有输入/关键状态/报告哈希冻结在
[执行证据](../baselines/audio-validation.json)。首轮 fixture 调用错误、Range 不完整、
WebKit 尺寸假设分别修正,没有增加超时或删掉真实播放/seek 断言。
最初定向 lint 有换行/多语句格式错误,按现有规范修正;受控测试多宿主改为单次全局 Audio
安装,避免多组恢复钩子的顺序污染,最终 CI 通过。
## 兼容、缺陷与范围
生产源码/声明/构建产物没有变更,没有新增 API、DOM/CSS 或消费者依赖。
AUDIO-LIFE-01:发布版保留 update 可在 destroy 后写 src,且空 src 清理导致浏览器 error=4。
AUDIO-SYNC-01:真实旧核心及候选核心中,原生暂停/自然结束后外部音频仍继续播放。
两项保持 open,由 03 修复;本步仅把证据等级更新为已复现。
负偏移起点、超时长、真实缓冲、快速切源、设备策略仍需 03/05 接续。
WAV 在 Windows WebKit 的原生 Audio 也失败,AAC 通过不替代 WAV 或真实 Safari 认证。
未测扬声器声音、物理设备和隔离安装包;不以本次基线关闭包发布门槛。
实际生产模块地图随 03 源码变化交付,本步提供可重跑维护入口。
## 提交与回退
独立完成提交主题:`test(audio): [PKG-AUDIO-02] cover released behavior and real media`。
回退本提交可移除新增测试/fixture/脚本接入和文档,生产播放器不受影响。
下一步 03 开始同步与生命周期源码拆分/修复,04 接续严格 TS/公开类型。
未 push/tag/publish/merge,全项目目标保持。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 216 项,范围 22 个包及工作区/示例。 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 216 项,范围 22 个包及工作区/示例。
状态:todo 149 / doing 1 / blocked 0 / done 66 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 状态:todo 148 / doing 1 / blocked 0 / done 67 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -156,7 +156,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 | | ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| PKG-AUDIO-01 | artplayer-plugin-audio-track<br>核对包契约与历史用法 | BASE-05 | url/offset/sync、audio 实例暴露、update 与音量语义 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done | | PKG-AUDIO-01 | artplayer-plugin-audio-track<br>核对包契约与历史用法 | BASE-05 | url/offset/sync、audio 实例暴露、update 与音量语义 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-AUDIO-02 | artplayer-plugin-audio-track<br>建立特有行为与错误测试 | PKG-AUDIO-01, ENG-03, ENG-05 | 偏移、倍率、seek、waiting/playing、加载错误和 autoplay 拒绝 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo | | 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 | todo | | PKG-AUDIO-03 | artplayer-plugin-audio-track<br>整理内部职责与资源 | PKG-AUDIO-02, CORE-10 | 外部 audio 的同步策略、监听与源更新生命周期 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-AUDIO-04 | artplayer-plugin-audio-track<br>迁移自有源码和公开类型 | PKG-AUDIO-03, ENG-04, ENG-06, CORE-07 | Option/Result/audio、update 输入和事件类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo | | PKG-AUDIO-04 | artplayer-plugin-audio-track<br>迁移自有源码和公开类型 | PKG-AUDIO-03, ENG-04, ENG-06, CORE-07 | Option/Result/audio、update 输入和事件类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| 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 | 主视频切源和缓冲恢复,多实例独立音频、旧核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
@@ -467,6 +467,7 @@
- PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json) - PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json)
- PKG-CHAPTER-04: [记录](changes/2026-09-10-PKG-CHAPTER-04-public-types.md) [记录](baselines/chapter-types-validation.json) - PKG-CHAPTER-04: [记录](changes/2026-09-10-PKG-CHAPTER-04-public-types.md) [记录](baselines/chapter-types-validation.json)
- PKG-AUDIO-01: [记录](changes/2026-09-12-PKG-AUDIO-01-contract.md) [记录](baselines/audio-track-contract.md) [记录](baselines/audio-track-release.json) - PKG-AUDIO-01: [记录](changes/2026-09-12-PKG-AUDIO-01-contract.md) [记录](baselines/audio-track-contract.md) [记录](baselines/audio-track-release.json)
- PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.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-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-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) - 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 @@
# 进度与证据 # 进度与证据
## 最新完成:PKG-AUDIO-02 行为与真实媒体基线
38 项 Node 契约覆盖源码和发布三格式,完整 CI 577 项、254 个生产 TS 通过。
三引擎最终 36 项通过、零失败/重试/跳过;其中 9 项为旧版缺陷复现、3 项为原生 WAV
能力诊断,不能把这些数量全部作为无缺陷候选验收。真实 AAC/视频播放、偏移 seek、
音量倍率、音频切源/503 恢复已验证。原始夹具失败与归因/trace 均保留。
旧插件销毁后 update 可复活、src='' 新生媒体错误、原生视频暂停/结束未停外部音频
已有证据;AUDIO-LIFE-01/SYNC-01 仍 open,03 接续源码拆分修复。
当前 216 项、67 done、1 doing、148 todo;HLS 仍保持原发布门槛。
Audio-01 已提交 8c0a2fee,Audio-02 独立本地提交后核实,再进入 03。
详见 [测试说明](audio-validation.md) 和 [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md)。
## 最新完成:PKG-AUDIO-01 外部音轨发布契约 ## 最新完成:PKG-AUDIO-01 外部音轨发布契约
冻结 npm audio-track 1.1.0 六文件及完整性,manifest/README/声明和起点一致, 冻结 npm audio-track 1.1.0 六文件及完整性,manifest/README/声明和起点一致,
+2 -2
View File
@@ -126,7 +126,7 @@
| HLS-ENV-01 | open / 已复现 | Windows WebKit 缺失 MSE,HLS 播放矩阵尚缺 Safari 证据 | PKG-HLS-05, REL-09, REVIEW-02 | | HLS-ENV-01 | open / 已复现 | Windows WebKit 缺失 MSE,HLS 播放矩阵尚缺 Safari 证据 | PKG-HLS-05, REL-09, REVIEW-02 |
| HLS-CRASH-01 | open / 已复现 | Firefox Hls 1.5.17 worker 销毁可在原生 video 与候选核心复现崩溃 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 | | HLS-CRASH-01 | open / 已复现 | Firefox Hls 1.5.17 worker 销毁可在原生 video 与候选核心复现崩溃 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 |
| HLS-PLAYBACK-01 | open / 已复现 | Firefox 旧核心与 Hls 1.7.2 切组后视频档位未完成切换 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 | | HLS-PLAYBACK-01 | open / 已复现 | Firefox 旧核心与 Hls 1.7.2 切组后视频档位未完成切换 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 |
| AUDIO-LIFE-01 | open / 源码/产物事实 | Audio Track 销毁后 update 与媒体/订阅缺少终止守卫 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 | | AUDIO-LIFE-01 | open / 已复现 | Audio Track 销毁后 update 与媒体/订阅缺少终止守卫 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 |
| AUDIO-SYNC-01 | open / 待取证 | Audio Track 偏移边界和切源/缓冲恢复尚缺真实时序证据 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 | | AUDIO-SYNC-01 | open / 已复现 | Audio Track 未同步原生暂停/结束,偏移边界与切源/缓冲仍待完整验证 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 |
| AUDIO-TYPE-01 | open / 源码/产物事实 | Audio Track update 部分配置与公开/编辑器声明不一致 | PKG-AUDIO-04 | | AUDIO-TYPE-01 | open / 源码/产物事实 | Audio Track update 部分配置与公开/编辑器声明不一致 | PKG-AUDIO-04 |
| AUDIO-DEMO-01 | open / 源码/产物事实 | Audio Track README 示例链接名称不匹配且缺维护说明 | PKG-AUDIO-06 | | AUDIO-DEMO-01 | open / 源码/产物事实 | Audio Track README 示例链接名称不匹配且缺维护说明 | PKG-AUDIO-06 |
+11 -5
View File
@@ -2647,7 +2647,7 @@
{ {
"id": "AUDIO-LIFE-01", "id": "AUDIO-LIFE-01",
"title": "Audio Track 销毁后 update 与媒体/订阅缺少终止守卫", "title": "Audio Track 销毁后 update 与媒体/订阅缺少终止守卫",
"confirmation": "source-observed", "confirmation": "reproduced",
"status": "open", "status": "open",
"owners": [ "owners": [
"PKG-AUDIO-02", "PKG-AUDIO-02",
@@ -2656,15 +2656,18 @@
], ],
"evidence": [ "evidence": [
"refactor/baselines/audio-track-contract.md", "refactor/baselines/audio-track-contract.md",
"refactor/baselines/audio-track-release.json" "refactor/baselines/audio-track-release.json",
"test/audio-track.test.js",
"test/browser/audio-track.spec.js",
"refactor/baselines/audio-validation.json"
], ],
"compatibleResolution": "保留公开 audio 和同步 update,封闭销毁后的资源写入并管理插件订阅,验证待决播放拒绝。", "compatibleResolution": "保留公开 audio 和同步 update,封闭销毁后的资源写入并管理插件订阅,验证待决播放拒绝。",
"closureCriteria": "受控复现和新旧核心真实媒体切源/销毁验证通过,不以源码观察代替泄漏证据。" "closureCriteria": "受控复现和新旧核心真实媒体切源/销毁验证通过,不以源码观察代替泄漏证据。"
}, },
{ {
"id": "AUDIO-SYNC-01", "id": "AUDIO-SYNC-01",
"title": "Audio Track 偏移边界和切源/缓冲恢复尚缺真实时序证据", "title": "Audio Track 未同步原生暂停/结束,偏移边界与切源/缓冲仍待完整验证",
"confirmation": "unverified", "confirmation": "reproduced",
"status": "open", "status": "open",
"owners": [ "owners": [
"PKG-AUDIO-02", "PKG-AUDIO-02",
@@ -2673,7 +2676,10 @@
], ],
"evidence": [ "evidence": [
"refactor/baselines/audio-track-contract.md", "refactor/baselines/audio-track-contract.md",
"refactor/baselines/audio-track-release.json" "refactor/baselines/audio-track-release.json",
"test/audio-track.test.js",
"test/browser/audio-track.spec.js",
"refactor/baselines/audio-validation.json"
], ],
"compatibleResolution": "固定严格阈值、偏移正负、倍率和音量语义;实测负目标/超时长及连续切源,按证据修复。", "compatibleResolution": "固定严格阈值、偏移正负、倍率和音量语义;实测负目标/超时长及连续切源,按证据修复。",
"closureCriteria": "正常/边界/失败/缓冲恢复有媒体与受控断言,实际支持环境缺口明确处理。" "closureCriteria": "正常/边界/失败/缓冲恢复有媒体与受控断言,实际支持环境缺口明确处理。"
+6 -2
View File
@@ -1490,11 +1490,15 @@
"ENG-03", "ENG-03",
"ENG-05" "ENG-05"
], ],
"status": "todo", "status": "done",
"risk": "H", "risk": "H",
"deliverable": "偏移、倍率、seek、waiting/playing、加载错误和 autoplay 拒绝", "deliverable": "偏移、倍率、seek、waiting/playing、加载错误和 autoplay 拒绝",
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言", "acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
"evidence": [] "evidence": [
"changes/2026-09-12-PKG-AUDIO-02-tests.md",
"audio-validation.md",
"baselines/audio-validation.json"
]
}, },
{ {
"id": "PKG-AUDIO-03", "id": "PKG-AUDIO-03",
+5
View File
@@ -40,3 +40,8 @@ Blob URL contents and cleanup when a controlled click fails.
See [browser/README.md](browser/README.md) for `yarn test:browser`, browser installation, See [browser/README.md](browser/README.md) for `yarn test:browser`, browser installation,
published/current combinations, media fixtures, candidate mapping and failure reports. published/current combinations, media fixtures, candidate mapping and failure reports.
These tests are separate from the fast Node suite and run in the Browser playback smoke CI job. These tests are separate from the fast Node suite and run in the Browser playback smoke CI job.
`test/audio-track.test.js` compares source and published Audio Track 1.1.0 in all three
formats with a controlled Audio object: timing intent, offsets, source updates, rejection,
volume/rate, independent instances and cleanup. Published-only tests retain defect evidence.
See [audio validation](../refactor/audio-validation.md) for actual media tests and limitations.
+167
View File
@@ -0,0 +1,167 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Controlled media contracts use Node's runner.
import { test } from 'node:test'
import { audioHost, audioImplementations, createAudioArt } from './helpers/audio-track.js'
const implementations = await audioImplementations()
for (const { name, factory } of implementations) {
test(`${name}: synchronous lazy installation, exposed audio and initial media properties`, (t) => {
const { art, instances } = audioHost(t)
const option = { url: 'first.aac' }
const attach = factory(option)
assert.equal(instances.length, 0)
option.url = 'before-install.aac'
const plugin = attach(art)
assert.equal(plugin.name, 'artplayerPluginAudioTrack')
assert.deepEqual(Object.keys(plugin).sort(), ['audio', 'name', 'update'])
assert.equal(plugin.audio, instances[0])
assert.deepEqual(plugin.audio.calls, [['preload', 'auto'], ['src', 'before-install.aac'], ['volume', 0.6], ['muted', false], ['playbackRate', 1.25]])
assert.equal(plugin.update({}), undefined)
assert.equal(art.muted, false, 'Plugin must not mute the main video')
})
test(`${name}: default threshold is strict and offset applies in both directions`, (t) => {
const { art } = audioHost(t)
const { audio, update } = factory({ url: 'audio.aac' })(art)
audio.calls.length = 0
art.currentTime = 0.3
art.emit('seek')
assert.deepEqual(audio.calls, [])
art.currentTime = 0.31
art.emit('seek')
assert.deepEqual(audio.calls, [['currentTime', 0.31]])
update({ offset: 2, sync: 0 })
art.currentTime = 4
art.emit('seek')
assert.equal(audio.currentTime, 6)
update({ offset: -1 })
art.emit('seek')
assert.equal(audio.currentTime, 3)
})
test(`${name}: play synchronizes before playback and timeupdate only follows playing video`, (t) => {
const { art } = audioHost(t)
const { audio } = factory({ url: 'audio.aac' })(art)
audio.calls.length = 0
art.currentTime = 4
art.emit('video:timeupdate')
assert.deepEqual(audio.calls, [])
art.emit('play')
assert.deepEqual(audio.calls, [['currentTime', 4], ['play']])
art.playing = true
art.currentTime = 5
art.emit('video:timeupdate')
assert.equal(audio.currentTime, 5)
art.video = null
audio.calls.length = 0
art.emit('seek')
assert.deepEqual(audio.calls, [])
})
test(`${name}: waiting pauses, playing resumes conditionally, pause always stops audio`, (t) => {
const { art } = audioHost(t)
const { audio } = factory({ url: 'audio.aac' })(art)
audio.calls.length = 0
art.emit('video:waiting').emit('video:playing')
assert.deepEqual(audio.calls, [['pause']])
art.playing = true
art.emit('video:playing').emit('pause')
assert.deepEqual(audio.calls, [['pause'], ['play'], ['pause']])
})
test(`${name}: independent volume mute and rate changes follow the host`, (t) => {
const { art } = audioHost(t)
const { audio } = factory({ url: 'audio.aac' })(art)
art.volume = 0
art.muted = true
art.video.playbackRate = 2
art.emit('video:volumechange').emit('video:ratechange')
assert.equal(audio.volume, 0)
assert.equal(audio.muted, true)
assert.equal(audio.playbackRate, 2)
art.video = undefined
assert.equal(factory({ url: '' })(art).audio.playbackRate, 1)
})
test(`${name}: same and empty URLs do not reload; changed source preserves audio identity`, (t) => {
const { art, instances } = audioHost(t)
const plugin = factory({ url: 'audio.aac' })(art)
plugin.audio.calls.length = 0
plugin.update({ url: 'audio.aac' })
plugin.update({ url: '' })
assert.deepEqual(plugin.audio.calls, [])
assert.equal(plugin.update({ url: 'paused.aac' }), undefined)
assert.deepEqual(plugin.audio.calls, [['src', 'paused.aac']])
art.playing = true
plugin.audio.calls.length = 0
plugin.update({ url: 'playing.aac' })
assert.deepEqual(plugin.audio.calls, [['src', 'playing.aac'], ['play']])
assert.equal(plugin.audio, instances[0])
assert.equal(instances.length, 1)
})
test(`${name}: an empty initial URL stays inert until configured`, (t) => {
const { art } = audioHost(t)
const { audio, update } = factory({ url: '' })(art)
art.currentTime = 3
art.playing = true
audio.calls.length = 0
art.emit('play').emit('seek').emit('video:timeupdate').emit('video:playing')
assert.deepEqual(audio.calls, [])
update({ url: 'configured.aac' })
assert.deepEqual(audio.calls, [['src', 'configured.aac'], ['play']])
})
test(`${name}: rejection at all three play sites is observed as the original warning`, async (t) => {
const { art, setPlay, warnings } = audioHost(t)
const plugin = factory({ url: 'audio.aac' })(art)
const rejection = new Error('Controlled NotAllowedError')
setPlay(() => Promise.reject(rejection))
art.playing = true
art.emit('play')
await Promise.resolve()
art.emit('video:playing')
await Promise.resolve()
assert.equal(plugin.update({ url: 'next.aac' }), undefined)
await Promise.resolve()
assert.deepEqual(warnings, [rejection, rejection, rejection])
})
test(`${name}: destroy releases the exposed media and multiple hosts stay independent`, (t) => {
const first = audioHost(t)
const second = createAudioArt()
const left = factory({ url: 'left.aac' })(first.art)
const right = factory({ url: 'right.aac' })(second.art)
assert.notEqual(left.audio, right.audio)
left.audio.calls.length = 0
right.audio.calls.length = 0
first.art.emit('destroy')
assert.deepEqual(left.audio.calls, [['pause'], ['src', ''], ['load']])
assert.deepEqual(right.audio.calls, [])
second.art.currentTime = 2
second.art.emit('seek')
assert.equal(right.audio.currentTime, 2)
})
}
const historical = implementations.find(item => item.name === 'published-main').factory
test('published defect AUDIO-LIFE-01: retained update reloads audio after destroy', (t) => {
const { art, listeners } = audioHost(t)
const plugin = historical({ url: 'audio.aac' })(art)
art.emit('destroy')
plugin.audio.calls.length = 0
plugin.update({ url: 'revived.aac' })
assert.deepEqual(plugin.audio.calls, [['src', 'revived.aac']])
assert.equal([...listeners.values()].reduce((count, set) => count + set.size, 0), 9)
})
test('published observation: offset update is deferred, negative target is written without clamping', (t) => {
const { art } = audioHost(t)
const plugin = historical({ url: 'audio.aac' })(art)
plugin.audio.calls.length = 0
plugin.update({ offset: -2 })
assert.deepEqual(plugin.audio.calls, [])
art.currentTime = 0
art.emit('seek')
assert.deepEqual(plugin.audio.calls, [['currentTime', -2]])
})
+5
View File
@@ -1,5 +1,10 @@
# Browser regression entry # Browser regression entry
Audio Track tests use the source and frozen 1.1.0 plugin on both cores. The local AAC fixture
has a pinned hash and generation command in media/audio-tone.json and uses the existing Range
server. The suite retains published cleanup/native-pause defects as explicit observations.
See [audio validation](../../refactor/audio-validation.md) for passed scope and device limitations.
Use the pinned Node and Yarn versions, then run: Use the pinned Node and Yarn versions, then run:
```sh ```sh
+206
View File
@@ -0,0 +1,206 @@
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'
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 = await compilePackage(release.name, 'umd')
evidence = { release, sourceSHA256: hash(sourceCode), 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 browser baseline; 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, 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.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 ['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)
}
test('native WAV diagnostic isolates sample decoding from ArtPlayer and plugin', async ({ page }, testInfo) => {
await page.route('**/native-wave.wav', route => route.fulfill({ contentType: 'audio/wav', body: wav }))
await page.goto('/test/player.html')
await page.evaluate(() => {
window.nativeAudio = new Audio()
window.nativeAudio.muted = true
window.nativeAudio.src = '/native-wave.wav'
})
await expect.poll(() => page.evaluate(() => window.nativeAudio.readyState >= 2 || Boolean(window.nativeAudio.error))).toBe(true)
const state = await page.evaluate(() => ({ canPlay: window.nativeAudio.canPlayType('audio/wav'), readyState: window.nativeAudio.readyState, error: window.nativeAudio.error?.code || null, playerCreated: Boolean(window.art) }))
expect(state.playerCreated).toBe(false)
await testInfo.attach('native-wav-capability', { contentType: 'application/json', body: JSON.stringify(state) })
if (!state.error) {
await page.evaluate(() => window.nativeAudio.play())
await expect.poll(() => page.evaluate(() => window.nativeAudio.currentTime)).toBeGreaterThan(0.2)
}
else {
expect(state.error).toBe(4)
}
await page.evaluate(() => {
window.nativeAudio.pause()
window.nativeAudio.removeAttribute('src')
window.nativeAudio.load()
})
})
test('published AUDIO-LIFE-01: empty src cleanup produces a native media error', async ({ page }, testInfo) => {
await openAudio(page, 'published', 'published', testInfo)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(0.2)
await page.evaluate(() => window.art.destroy())
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.error?.code)).toBe(4)
expect(await page.evaluate(() => window.audioPlugin.audio.getAttribute('src'))).toBe('')
expect(await page.evaluate(() => window.audioPlugin.audio.readyState)).toBe(0)
})
for (const core of ['published', 'candidate']) {
test(`${core} core + published audio: native pause and end leave external audio playing`, async ({ page }, testInfo) => {
await openAudio(page, core, 'published', testInfo)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(0.2)
await page.evaluate(() => window.art.video.pause())
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(false)
await page.evaluate(() => {
window.art.seek = 7.8
window.art.play()
})
await expect.poll(() => page.evaluate(() => window.art.video.ended)).toBe(true)
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(false)
await testInfo.attach('published-native-pause-end', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ videoEnded: window.art.video.ended, videoPaused: window.art.video.paused, audioPaused: window.audioPlugin.audio.paused, audioTime: window.audioPlugin.audio.currentTime }))) })
await page.evaluate(() => window.art.destroy())
})
for (const plugin of ['published', 'source']) {
test(`${core} core + ${plugin} audio: decoded play, offset seek, rate, source and cleanup`, 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 expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
expect(await page.evaluate(() => window.audioPlugin.audio.duration)).toBe(16)
// Windows WebKit may expose layout size; decoded pixels are the playback evidence.
const pixel = await page.evaluate(() => {
const canvas = document.createElement('canvas')
canvas.width = 80
canvas.height = 45
const context = canvas.getContext('2d')
context.drawImage(window.art.video, 0, 0, 80, 45)
return [...context.getImageData(30, 2, 1, 1).data]
})
expect(pixel[0]).toBeGreaterThan(180)
expect(pixel[1]).toBeGreaterThan(180)
expect(pixel[2]).toBeLessThan(80)
await testInfo.attach('decoded-frame-pixel', { contentType: 'application/json', body: JSON.stringify(pixel) })
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.seekable.length && window.audioPlugin.audio.seekable.end(0))).toBeGreaterThan(2.25)
await page.evaluate(() => {
window.art.pause()
window.audioPlugin.update({ offset: 0.25 })
window.art.seek = 2
})
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeCloseTo(2.25, 1)
await page.evaluate(() => {
window.art.playbackRate = 1.5
window.art.volume = 0.4
})
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.playbackRate)).toBe(1.5)
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.volume)).toBeCloseTo(0.4)
await page.locator('#play').click()
await page.evaluate(() => {
window.originalAudio = window.audioPlugin.audio
window.audioPlugin.update({ url: '/test/audio-tone.m4a?source=second' })
})
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentSrc)).toContain('source=second')
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.paused)).toBe(false)
await expect.poll(() => page.evaluate(() => Math.abs(window.audioPlugin.audio.currentTime - window.art.currentTime - 0.25))).toBeLessThan(0.5)
expect(await page.evaluate(() => window.originalAudio === window.audioPlugin.audio)).toBe(true)
await page.evaluate(() => {
window.art.pause()
window.art.destroy()
})
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBe(0)
expect(await page.evaluate(() => window.audioPlugin.audio.getAttribute('src'))).toBe('')
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
expect(await page.locator('.art-video-player').count()).toBe(0)
})
test(`${core} core + ${plugin} audio: real load error can recover with a new source`, async ({ page }, testInfo) => {
await openAudio(page, core, plugin, testInfo, '')
await page.route('**/audio-fixture/broken.m4a', route => route.fulfill({ status: 503, body: 'Intentional media failure' }))
await page.evaluate(() => window.audioPlugin.update({ url: '/audio-fixture/broken.m4a' }))
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.error?.code)).toBe(4)
expect(await page.evaluate(() => window.art.video.error)).toBeNull()
await page.evaluate(() => window.audioPlugin.update({ url: '/test/audio-tone.m4a?source=recovered' }))
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBeGreaterThanOrEqual(2)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(0.3)
expect(await page.evaluate(() => window.audioPlugin.audio.error)).toBeNull()
await page.evaluate(() => window.art.destroy())
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBe(0)
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
expect(await page.evaluate(() => window.audioPlugin.audio.getAttribute('src'))).toBe('')
})
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"schemaVersion": 1,
"file": "audio-tone.m4a",
"sha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
"bytes": 132436,
"command": "ffmpeg -hide_banner -loglevel error -f lavfi -i sine=frequency=440:sample_rate=48000:duration=16 -ac 1 -c:a aac -b:a 64k -map_metadata -1 -movflags +faststart test/browser/media/audio-tone.m4a",
"ffmpeg": "ffmpeg version 2025-11-12-git-6cdd2cbe32-full_build-www.gyan.dev Copyright (c) 2000-2025 the FFmpeg developers",
"probe": {
"programs": [],
"stream_groups": [],
"streams": [
{
"codec_name": "aac",
"codec_type": "audio",
"sample_rate": "48000",
"channels": 1
}
],
"format": {
"duration": "16.000000"
}
}
}
Binary file not shown.
+2 -1
View File
@@ -59,8 +59,9 @@ for (const extension of ['mp4', 'webm']) {
const name = `test/browser/media/pattern.${extension}` const name = `test/browser/media/pattern.${extension}`
add(`/test/pattern.${extension}`, fs.readFileSync(path.join(workspace, name)), { kind: 'generated-media', file: name }) add(`/test/pattern.${extension}`, fs.readFileSync(path.join(workspace, name)), { kind: 'generated-media', file: name })
} }
add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/media/audio-tone.m4a')), { kind: 'generated-media', file: 'test/browser/media/audio-tone.m4a' })
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' } const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/browser/media/declaration-cues.vtt')), { kind: 'test-subtitles', file: 'test/browser/media/declaration-cues.vtt' }) add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/browser/media/declaration-cues.vtt')), { kind: 'test-subtitles', file: 'test/browser/media/declaration-cues.vtt' })
add('/test/thumbnail-grid.svg', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-grid.svg')), { kind: 'test-thumbnail-grid', file: 'test/browser/media/thumbnail-grid.svg' }) add('/test/thumbnail-grid.svg', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-grid.svg')), { kind: 'test-thumbnail-grid', file: 'test/browser/media/thumbnail-grid.svg' })
add('/test/legacy-safe-area.js', fs.readFileSync(path.join(workspace, 'test/helpers/legacy-safe-area.js')), { kind: 'frozen-own-source', file: 'test/helpers/legacy-safe-area.js', commit: 'ccf77c4e' }) add('/test/legacy-safe-area.js', fs.readFileSync(path.join(workspace, 'test/helpers/legacy-safe-area.js')), { kind: 'frozen-own-source', file: 'test/helpers/legacy-safe-area.js', commit: 'ccf77c4e' })
+99
View File
@@ -0,0 +1,99 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import vm from 'node:vm'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { loadPackage } from './load.js'
export async function audioImplementations() {
const { release } = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/audio-track-release.json', import.meta.url)))
const archive = await ensureArchive(release)
const implementations = [{ name: 'source', factory: (await loadPackage(release.name)).default }]
for (const format of ['main', 'legacy', 'module']) {
const member = `package/${release.manifest[format].replace(/^\.\//, '')}`
const bytes = readMember(archive, member)
assert.equal(hash(bytes), release.files[member])
let factory
if (format === 'module') {
factory = (await import(`data:text/javascript;base64,${Buffer.from(bytes).toString('base64')}`)).default
}
else {
const context = {
module: { exports: {} },
Audio: function Audio() {
return new globalThis.Audio()
},
console,
}
context.exports = context.module.exports
vm.runInNewContext(bytes.toString(), context, { timeout: 5000 })
factory = context.module.exports
}
implementations.push({ name: `published-${format}`, factory })
}
return implementations
}
// Records plugin intent only. It does not decode media or emulate browser timing.
export function audioHost(t) {
const instances = []
const warnings = []
let play = () => Promise.resolve()
class Audio {
constructor() {
instances.push(this)
this.calls = []
this.paused = true
for (const [name, initial] of Object.entries({ src: '', preload: '', currentTime: 0, playbackRate: 1, volume: 1, muted: false })) {
let value = initial
Object.defineProperty(this, name, {
get: () => value,
set: (next) => {
this.calls.push([name, next])
value = next
},
})
}
}
play() {
this.calls.push(['play'])
return play(this)
}
pause() { this.calls.push(['pause']) }
load() { this.calls.push(['load']) }
}
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'Audio')
Object.defineProperty(globalThis, 'Audio', { configurable: true, value: Audio })
t.after(() => descriptor ? Object.defineProperty(globalThis, 'Audio', descriptor) : Reflect.deleteProperty(globalThis, 'Audio'))
t.mock.method(console, 'warn', error => warnings.push(error))
return { ...createAudioArt(), instances, warnings, setPlay: callback => play = callback }
}
export function createAudioArt() {
const listeners = new Map()
const art = {
video: { playbackRate: 1.25 },
currentTime: 0,
playing: false,
volume: 0.6,
muted: false,
on(event, callback) {
if (!listeners.has(event))
listeners.set(event, new Set())
listeners.get(event).add(callback)
return art
},
off(event, callback) {
listeners.get(event)?.delete(callback)
return art
},
emit(event, ...args) {
for (const callback of [...(listeners.get(event) || [])])
callback(...args)
return art
},
}
return { art, listeners }
}