mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-12 15:26:16 -08:00
test(audio): [PKG-AUDIO-02] cover released behavior and real media
This commit is contained in:
1 parent
8c0a2feed3
commit
803e87252e
18 files changed
+8519
-13
No files matched your search
@@ -3,4 +3,5 @@
|
||||
test/browser/media/hls/*.m3u8 text eol=lf
|
||||
test/browser/fixtures/hls-grouped.m3u8 text eol=lf
|
||||
test/browser/media/hls/*.mpegts binary
|
||||
test/browser/media/*.m4a binary
|
||||
refactor/baselines/core-vendor/*.txt text eol=lf
|
||||
+1
-1
@@ -50,7 +50,7 @@
|
||||
"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",
|
||||
"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": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
|
||||
@@ -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
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`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。
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 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-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 |
|
||||
@@ -467,6 +467,7 @@
|
||||
- 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-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-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)
|
||||
|
||||
@@ -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 外部音轨发布契约
|
||||
|
||||
冻结 npm audio-track 1.1.0 六文件及完整性,manifest/README/声明和起点一致,
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
| 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-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-SYNC-01 | open / 待取证 | Audio Track 偏移边界和切源/缓冲恢复尚缺真实时序证据 | 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-TYPE-01 | open / 源码/产物事实 | Audio Track update 部分配置与公开/编辑器声明不一致 | PKG-AUDIO-04 |
|
||||
| AUDIO-DEMO-01 | open / 源码/产物事实 | Audio Track README 示例链接名称不匹配且缺维护说明 | PKG-AUDIO-06 |
|
||||
+11
-5
@@ -2647,7 +2647,7 @@
|
||||
{
|
||||
"id": "AUDIO-LIFE-01",
|
||||
"title": "Audio Track 销毁后 update 与媒体/订阅缺少终止守卫",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-AUDIO-02",
|
||||
@@ -2656,15 +2656,18 @@
|
||||
],
|
||||
"evidence": [
|
||||
"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,封闭销毁后的资源写入并管理插件订阅,验证待决播放拒绝。",
|
||||
"closureCriteria": "受控复现和新旧核心真实媒体切源/销毁验证通过,不以源码观察代替泄漏证据。"
|
||||
},
|
||||
{
|
||||
"id": "AUDIO-SYNC-01",
|
||||
"title": "Audio Track 偏移边界和切源/缓冲恢复尚缺真实时序证据",
|
||||
"confirmation": "unverified",
|
||||
"title": "Audio Track 未同步原生暂停/结束,偏移边界与切源/缓冲仍待完整验证",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-AUDIO-02",
|
||||
@@ -2673,7 +2676,10 @@
|
||||
],
|
||||
"evidence": [
|
||||
"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": "固定严格阈值、偏移正负、倍率和音量语义;实测负目标/超时长及连续切源,按证据修复。",
|
||||
"closureCriteria": "正常/边界/失败/缓冲恢复有媒体与受控断言,实际支持环境缺口明确处理。"
|
||||
|
||||
+6
-2
@@ -1490,11 +1490,15 @@
|
||||
"ENG-03",
|
||||
"ENG-05"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "偏移、倍率、seek、waiting/playing、加载错误和 autoplay 拒绝",
|
||||
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-AUDIO-02-tests.md",
|
||||
"audio-validation.md",
|
||||
"baselines/audio-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-AUDIO-03",
|
||||
|
||||
@@ -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,
|
||||
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.
|
||||
|
||||
`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.
|
||||
@@ -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]])
|
||||
})
|
||||
@@ -1,5 +1,10 @@
|
||||
# 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:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -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('')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -59,8 +59,9 @@ for (const extension of ['mp4', 'webm']) {
|
||||
const name = `test/browser/media/pattern.${extension}`
|
||||
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/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' })
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in new issue
Block a user