mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-19 11:21:20 -08:00
test(dash): [PKG-DASH-02] cover released contracts and real menu interactions
This commit is contained in:
1 parent
3d4c636101
commit
762cc63431
12 files changed
+2580
-15
No files matched your search
+2
-2
@@ -36,7 +36,7 @@
|
||||
"test:playback": "node --test test/playback.test.js",
|
||||
"trans:docs": "node ./scripts/trans-docs.js",
|
||||
"create:plugin": "node ./scripts/plugin/create.js",
|
||||
"test:dash-control": "node --test test/dash-control.test.js",
|
||||
"test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js",
|
||||
"dev": "npx cross-env NODE_ENV=development node ./scripts/dev.js",
|
||||
"build": "npx cross-env NODE_ENV=production node ./scripts/build.js",
|
||||
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
|
||||
@@ -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/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:unit": "node --test test/playback.test.js test/dash-control.test.js test/dash-contract.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",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,22 @@
|
||||
# PKG-DASH-02:DASH 特有行为与错误测试
|
||||
|
||||
起点 `3d4c6361`,新增源码/冻结工作区/真实 npm 三格式行为测试和三引擎真实 DOM 交互测试。
|
||||
保留原有 5 项稳定 ID 回归,补全音轨、默认参数、排序去重、ABR、原始对象/调用身份、
|
||||
同步异常、换 SDK、多实例、空拓扑、销毁后引用与历史高亮问题。
|
||||
|
||||
维护入口及范围见 [验证说明](../dash-validation.md),输入/报告指纹见
|
||||
[执行证据](../baselines/dash-validation.json)。本步只建立测试,不改生产源码、公开类型、
|
||||
README、demo、构建产物或依赖。测试脚本接入已有 `test:unit` 和 `test:dash-control`。
|
||||
生产模块图将在 03/04 随实际拆分交付,不能提前描述尚未实现的结构。
|
||||
|
||||
定向 Node 74 项通过。浏览器首轮 24 通过/24 定位失败,trace 证明 SVG 换行使整行精确
|
||||
匹配失效;修正为专用文字节点,原有断言不变,最终 48 项通过、无重试/跳过。
|
||||
实际 SDK/MPD 解码不在这 48 项证据内,05 仍须验证两代固定 SDK。
|
||||
|
||||
完整 CI 670 项通过(619 单元、14 工程、37 基线),包括严格类型、声明漂移和 lint。
|
||||
最终任务/风险校验通过,217 项任务为 72 done、3 doing、142 todo。
|
||||
本步复现的 DASH-SDK-01、DASH-LIFE-01、DASH-STATE-01
|
||||
均保持 open,明确由 03/05 接续。没有以历史缺陷观察替代候选修复断言。
|
||||
|
||||
本任务独立本地 commit;回退本提交撤回新增测试、脚本接入和记录,生产行为不变。
|
||||
未 push/tag/publish/merge;全项目目标未完成。下一任务 PKG-DASH-03。
|
||||
@@ -0,0 +1,45 @@
|
||||
# DASH Control 验证与维护入口
|
||||
|
||||
PKG-DASH-02 保护真实 npm 1.1.0 的既有调用契约,以及重构前工作区的 SDK 5.x 稳定 ID
|
||||
修正。版本来源见 [发布契约](baselines/dash-control-contract.md)。本步没有更改生产实现。
|
||||
|
||||
## 可重跑的测试
|
||||
|
||||
- `yarn test:dash-control`:原有 5 项过滤/稳定 ID 回归和新增 69 项行为测试,共 74 项。
|
||||
该脚本和 `test:unit` 都保留旧测试,完整 CI 自动执行。
|
||||
- `yarn test:browser test/browser/dash-control.spec.js --trace on`:48 项,两个核心、两个插件、
|
||||
两类菜单、两种选择、Chromium/Firefox/WebKit。浏览器套件串行运行,避免共享端口和报告覆盖。
|
||||
- `yarn ci:check`:工具链、计划、lint、声明漂移、严格类型和 Node/基线测试。
|
||||
|
||||
Node helper 分别加载当前源码、固定 Git 提交的旧工作区 UMD、实际 npm main/legacy/module。
|
||||
Git 内容以 LF 指纹核对,发布归档与成员以原始字节指纹核对。`ARTPLAYER_TEST_DASH` 可指定
|
||||
待测产物,以系统路径分隔符连接;未设置时不会把源码测试冒充三种候选产物验证。
|
||||
|
||||
`test/helpers/dash-control.js` 只提供可控 SDK 方法和组件注册表,检查 this、类型参数、
|
||||
原始轨道身份、调用顺序、错误身份、ABR 配置保留、换 SDK、多实例和关闭后的引用。
|
||||
真实旧实现的缺陷观察保持冻结;03 修复时为候选实现新增正确行为断言,不把历史缺陷当作兼容要求。
|
||||
|
||||
浏览器使用真实发布/候选 ArtPlayer DOM、真实发布/候选插件和本地原生 MP4。SDK 方法可控,
|
||||
断言真实点击触发的参数及控制栏/设置同步,再通过真实播放按钮验证视频时钟推进、无媒体错误。
|
||||
**没有加载 dash.js,没有验证 MPD、ABR 自动切换或自适应流解码。** 这些仍是 05 必须完成的
|
||||
固定 dash.js 4.5.2 / 5.2.1 集成验证。`ARTPLAYER_DASH_ARTIFACT` 可替换插件脚本;本次没有使用。
|
||||
|
||||
## 首次失败与证据
|
||||
|
||||
首次浏览器运行 24 通过、24 失败;全部失败位于设置选项文字定位。trace 中选项确实已渲染,
|
||||
整行文本包含图标 SVG 的换行,`^fr$`/`^720p$` 不能匹配。改为当前面板内专用
|
||||
`.art-setting-item-left-text` 的精确文本定位;保持原有调用、同步和播放断言,没有增大超时或跳过。
|
||||
补充清理前行文本诊断,最终 48 项通过;候选设置选择的截图已人工查看,文字与控制栏一致。
|
||||
|
||||
首轮与最终报告、全部结果/trace 目录都保留在本地缓存;输入和证据指纹见
|
||||
[本步执行记录](baselines/dash-validation.json)。缓存不入 Git,永久保存的是测试、来源和结果摘要;
|
||||
换机器应按上述命令重新取得执行证据,不能声称本地 trace 已上传。
|
||||
|
||||
## 下一步缺陷与边界
|
||||
|
||||
- DASH-SDK-01:两代旧实现分别在另一代 SDK 方法表上失败。03 通过能力适配恢复旧用法。
|
||||
- DASH-LIFE-01:空列表/禁用后 UI 残留,旧回调仍写旧 SDK,销毁后 update 和订阅仍有效。
|
||||
- DASH-STATE-01:克隆当前轨道、重复标签、数字零 representation ID 会丢失高亮。
|
||||
|
||||
这些受控复现仍是 open,不能以本步绿色测试关闭。03 处理结构、清理与正确当前项,04 完成公开
|
||||
类型兼容,05 完成真实 SDK/媒体/组合验证,06 完成包分发验收。物理设备和 npm 发布准入仍待完成。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 143 / doing 3 / blocked 0 / done 71 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 142 / doing 3 / blocked 0 / done 72 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-DASH-01 | artplayer-plugin-dash-control<br>核对包契约与历史用法 | BASE-05 | quality/audio、representation ID/Auto、getName 和 update | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
|
||||
| PKG-DASH-02 | artplayer-plugin-dash-control<br>建立特有行为与错误测试 | PKG-DASH-01, ENG-03, ENG-05 | 保留已有 5 项稳定 ID 回归,补发布版 SDK 4.x/当前 5.x、音轨/空列表/过滤/换源与关闭引用 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
|
||||
| PKG-DASH-02 | artplayer-plugin-dash-control<br>建立特有行为与错误测试 | PKG-DASH-01, ENG-03, ENG-05 | 保留已有 5 项稳定 ID 回归,补发布版 SDK 4.x/当前 5.x、音轨/空列表/过滤/换源与关闭引用 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-DASH-03 | artplayer-plugin-dash-control<br>整理内部职责与资源 | PKG-DASH-02, CORE-11, CORE-14 | 能力适配保留 SDK 4.x 与 5.x,稳定 ID 映射、ABR 状态、UI 清理和生命周期职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-DASH-04 | artplayer-plugin-dash-control<br>迁移自有源码和公开类型 | PKG-DASH-03, ENG-04, ENG-06, CORE-07 | dash.js adapter、selector 和回调类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-DASH-05 | artplayer-plugin-dash-control<br>验证新旧核心和组合 | PKG-DASH-04, CORE-22 | 固定 dash.js 4.5.2/5.2.1 与本地 DASH 实际清晰度/音轨、高亮、Auto 和换源组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
@@ -480,3 +480,4 @@
|
||||
- PKG-HLS-04: [记录](changes/2026-09-12-PKG-HLS-04-types.md) [记录](baselines/hls-types-validation.json) [记录](hls-validation.md)
|
||||
- PKG-HLS-SDK-01: [记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) [记录](baselines/hls-sdk-validation.json) [记录](baselines/hls-sdk-diagnostics.json)
|
||||
- PKG-DASH-01: [记录](changes/2026-09-12-PKG-DASH-01-contract.md) [记录](baselines/dash-control-contract.md) [记录](baselines/dash-control-release.json)
|
||||
- PKG-DASH-02: [记录](changes/2026-09-12-PKG-DASH-02-tests.md) [记录](dash-validation.md) [记录](baselines/dash-validation.json)
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## 最新完成:DASH-02 行为测试与真实 DOM 交互
|
||||
|
||||
保留原有 5 项稳定 ID 回归,新增发布三格式/冻结工作区/候选源码契约测试,定向 74 项通过。
|
||||
完整 CI 670 项通过(619 单元、14 工程、37 基线),未修改生产源码、声明、依赖或产物。
|
||||
新旧核心/插件、控制栏/设置、质量/音轨三引擎最终 48 项通过。首轮 24 通过/24 失败的报告
|
||||
及全部 trace 已保留;失败原因是 SVG 换行使整行精确定位失效,改为专用文本节点,未放宽断言。
|
||||
|
||||
浏览器使用真实 DOM 和原生 MP4,SDK 方法受控;不代表 dash.js/MPD/ABR 实际集成通过。
|
||||
SDK 两代不兼容、过期 UI/引用、当前项高亮问题已受控复现,仍保持 open,03/05 接续修复验证。
|
||||
当前 217 项:72 done、3 doing、142 todo。下一步 DASH-03 开始源码职责和 SDK 适配改造。
|
||||
详见 [本次变更](changes/2026-09-12-PKG-DASH-02-tests.md) 与 [验证维护入口](dash-validation.md)。
|
||||
本任务独立本地提交,不推送/发布;其他媒体和设备风险、全包发布准入仍未完成。
|
||||
|
||||
## 最新完成:DASH-01 发布版与 SDK 兼容契约
|
||||
|
||||
已校验 DASH Control 1.1.0 的实际 npm 归档及 6 个成员。发现发布版使用 dash.js 4.x
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
| AUDIO-BUFFER-01 | open / 已复现 | Windows WebKit 原生媒体无法在受限响应下推进至真实缓冲 | PKG-AUDIO-05 |
|
||||
| CHAPTER-LAYOUT-01 | resolved / 已复现 | 长章节标题超出窄播放器进度条宽度 | PKG-CHAPTER-05 |
|
||||
| CHAPTER-TIMING-01 | open / 已复现 | Windows WebKit 清晰度切换曾在等待窗口内未见 restart,稍后状态恢复 | PKG-CHAPTER-05 |
|
||||
| DASH-SDK-01 | open / 源码/产物事实 | npm 发布版 4.x 与工作区仅 5.x 的 SDK 接口不兼容 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
|
||||
| DASH-LIFE-01 | open / 源码/产物事实 | DASH 空拓扑残留 UI,监听和旧选择回调缺乏关闭守卫 | PKG-DASH-02, PKG-DASH-03 |
|
||||
| DASH-STATE-01 | open / 源码/产物事实 | DASH 当前项高亮使用真值 ID、音轨身份和标签去重,边界待核实 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
|
||||
| DASH-SDK-01 | open / 已复现 | npm 发布版 4.x 与工作区仅 5.x 的 SDK 接口不兼容 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
|
||||
| DASH-LIFE-01 | open / 已复现 | DASH 空拓扑残留 UI,监听和旧选择回调缺乏关闭守卫 | PKG-DASH-02, PKG-DASH-03 |
|
||||
| DASH-STATE-01 | open / 已复现 | DASH 当前项高亮使用真值 ID、音轨身份和标签去重,边界待核实 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
|
||||
| DASH-TYPE-01 | open / 源码/产物事实 | DASH 类型未表达默认工厂参数、SDK 回调对象和现代模块入口 | PKG-DASH-04 |
|
||||
| DASH-DEMO-01 | open / 源码/产物事实 | DASH 示例仅新 SDK 且重复安装会累积销毁回调 | PKG-DASH-06 |
|
||||
+12
-6
@@ -2834,7 +2834,7 @@
|
||||
{
|
||||
"id": "DASH-SDK-01",
|
||||
"title": "npm 发布版 4.x 与工作区仅 5.x 的 SDK 接口不兼容",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-DASH-02",
|
||||
@@ -2846,7 +2846,9 @@
|
||||
"refactor/baselines/dash-control-release.json",
|
||||
"packages/artplayer-plugin-dash-control/src/index.js",
|
||||
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
|
||||
"docs/assets/example/dash.control.js"
|
||||
"docs/assets/example/dash.control.js",
|
||||
"test/dash-contract.test.js",
|
||||
"refactor/dash-validation.md"
|
||||
],
|
||||
"compatibleResolution": "按方法能力适配两代已存在用法,保留 v5 稳定 ID,不能要求旧消费者改用新 SDK。",
|
||||
"closureCriteria": "归档旧用法、v4/v5 受控及实际媒体矩阵通过,清晰度键不混用。"
|
||||
@@ -2854,7 +2856,7 @@
|
||||
{
|
||||
"id": "DASH-LIFE-01",
|
||||
"title": "DASH 空拓扑残留 UI,监听和旧选择回调缺乏关闭守卫",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-DASH-02",
|
||||
@@ -2865,7 +2867,9 @@
|
||||
"refactor/baselines/dash-control-release.json",
|
||||
"packages/artplayer-plugin-dash-control/src/index.js",
|
||||
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
|
||||
"docs/assets/example/dash.control.js"
|
||||
"docs/assets/example/dash.control.js",
|
||||
"test/dash-contract.test.js",
|
||||
"refactor/dash-validation.md"
|
||||
],
|
||||
"compatibleResolution": "仅释放插件自有 ready/restart/destroy 订阅和 UI,SDK 仍由调用方拥有;替换后旧回调不写旧实例。",
|
||||
"closureCriteria": "空列表、换源/SDK、销毁后引用、安装异常有回归,其他实例与 SDK 不被误释放。"
|
||||
@@ -2873,7 +2877,7 @@
|
||||
{
|
||||
"id": "DASH-STATE-01",
|
||||
"title": "DASH 当前项高亮使用真值 ID、音轨身份和标签去重,边界待核实",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-DASH-02",
|
||||
@@ -2885,7 +2889,9 @@
|
||||
"refactor/baselines/dash-control-release.json",
|
||||
"packages/artplayer-plugin-dash-control/src/index.js",
|
||||
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
|
||||
"docs/assets/example/dash.control.js"
|
||||
"docs/assets/example/dash.control.js",
|
||||
"test/dash-contract.test.js",
|
||||
"refactor/dash-validation.md"
|
||||
],
|
||||
"compatibleResolution": "验证零 ID、重复标签和克隆音轨当前项,在保持原展示语义时按实际可选项定位高亮。",
|
||||
"closureCriteria": "有受控和真实 SDK 证据说明选中的 representation/track 与 UI 一致。"
|
||||
|
||||
+6
-2
@@ -1992,11 +1992,15 @@
|
||||
"ENG-03",
|
||||
"ENG-05"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "保留已有 5 项稳定 ID 回归,补发布版 SDK 4.x/当前 5.x、音轨/空列表/过滤/换源与关闭引用",
|
||||
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-DASH-02-tests.md",
|
||||
"dash-validation.md",
|
||||
"baselines/dash-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-DASH-03",
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import assert from 'node:assert/strict'
|
||||
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
|
||||
test.beforeAll(async () => {
|
||||
const { release } = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/dash-control-release.json', import.meta.url)))
|
||||
|
||||
const member = 'package/dist/artplayer-plugin-dash-control.js'
|
||||
|
||||
const bytes = readMember(await ensureArchive(release), member)
|
||||
|
||||
assert.equal(hash(bytes), release.files[member])
|
||||
|
||||
publishedCode = bytes.toString()
|
||||
|
||||
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Controlled DASH source tests do not represent installed SDK/package acceptance')
|
||||
|
||||
sourceCode = process.env.ARTPLAYER_DASH_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_DASH_ARTIFACT, 'utf8') : await compilePackage(release.name, 'umd')
|
||||
|
||||
evidence = { published: release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_DASH_ARTIFACT || 'workspace source build', scope: 'Real ArtPlayer DOM and native MP4 with controlled SDK methods. No dash.js instance, MPD, ABR or adaptive media validation.' }
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
const state = await page.evaluate(() => ({
|
||||
calls: window.dashCalls,
|
||||
ready: window.art?.isReady,
|
||||
time: window.art?.currentTime,
|
||||
videoError: window.art?.video.error?.code || null,
|
||||
quality: window.art?.controls['dash-quality']?.textContent,
|
||||
audio: window.art?.controls['dash-audio']?.textContent,
|
||||
settingRows: [...document.querySelectorAll('.art-setting-panel.art-current .art-setting-item')].map(row => ({ text: row.textContent, label: row.querySelector('.art-setting-item-left-text')?.textContent })),
|
||||
})).catch(error => ({ error: error.message }))
|
||||
|
||||
await testInfo.attach('controlled-dash-state', { contentType: 'application/json', body: JSON.stringify(state) })
|
||||
|
||||
if (!page.isClosed())
|
||||
await page.evaluate(() => window.art?.destroy())
|
||||
})
|
||||
|
||||
async function openDash(page, core, plugin, testInfo) {
|
||||
await testInfo.attach('controlled-dash-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((sdk) => {
|
||||
window.dashCalls = []
|
||||
|
||||
window.art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: '/test/pattern.mp4',
|
||||
muted: true,
|
||||
setting: true,
|
||||
plugins: [(art) => {
|
||||
const levels = sdk === 4
|
||||
? [{ qualityIndex: 0, height: 360 }, { qualityIndex: 1, height: 720 }]
|
||||
: [{ id: 'low', height: 360 }, { id: 'high', height: 720 }]
|
||||
|
||||
const tracks = [{ id: 'en', lang: 'en' }, { id: 'fr', lang: 'fr' }]
|
||||
|
||||
window.dashState = { levels, tracks, track: tracks[0], current: levels[0], quality: 0, settings: { streaming: { abr: { autoSwitchBitrate: { video: true } } } } }
|
||||
|
||||
const state = window.dashState
|
||||
|
||||
art.dash = {
|
||||
getVideoElement: () => art.template.$video,
|
||||
getSettings: () => state.settings,
|
||||
updateSettings(update) {
|
||||
state.settings.streaming.abr.autoSwitchBitrate.video = update.streaming.abr.autoSwitchBitrate.video
|
||||
window.dashCalls.push(['auto', state.settings.streaming.abr.autoSwitchBitrate.video])
|
||||
},
|
||||
getTracksFor: () => state.tracks,
|
||||
getCurrentTrackFor: () => state.track,
|
||||
setCurrentTrack(track) {
|
||||
state.track = track
|
||||
window.dashCalls.push(['track', track.id, tracks.includes(track)])
|
||||
},
|
||||
}
|
||||
|
||||
if (sdk === 4) {
|
||||
Object.assign(art.dash, {
|
||||
getBitrateInfoListFor: () => levels,
|
||||
getQualityFor: () => state.quality,
|
||||
setQualityFor(type, index) {
|
||||
state.quality = index
|
||||
window.dashCalls.push(['qualityIndex', type, index])
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
Object.assign(art.dash, {
|
||||
getRepresentationsByType: () => levels,
|
||||
getCurrentRepresentationForType: () => state.current,
|
||||
setRepresentationForTypeById(type, id) {
|
||||
state.current = levels.find(level => level.id === id)
|
||||
window.dashCalls.push(['representationId', type, id])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return window.artplayerPluginDashControl({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })(art)
|
||||
}],
|
||||
})
|
||||
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
|
||||
document.querySelector('#pause').onclick = () => window.art.pause()
|
||||
}, plugin === 'published' ? 4 : 5)
|
||||
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
|
||||
await expect(page.locator('.art-control-dash-quality .art-selector-value')).toHaveText('Auto')
|
||||
|
||||
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('en')
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const plugin of ['published', 'candidate']) {
|
||||
for (const surface of ['control', 'setting']) {
|
||||
for (const type of ['quality', 'audio']) {
|
||||
test(`${core} core + ${plugin} DASH: controlled SDK ${type} selection through real ${surface} DOM`, async ({ page }, testInfo) => {
|
||||
await openDash(page, core, plugin, testInfo)
|
||||
|
||||
const text = type === 'quality' ? '720p' : 'fr'
|
||||
|
||||
if (surface === 'control') {
|
||||
const control = page.locator(`.art-control-dash-${type}`)
|
||||
|
||||
await control.hover()
|
||||
|
||||
await expect(control.locator('.art-selector-list')).toHaveCSS('opacity', '1')
|
||||
|
||||
await control.locator('.art-selector-item').filter({ hasText: new RegExp(`^${text}$`) }).click()
|
||||
}
|
||||
else {
|
||||
await page.locator('.art-control-setting').click()
|
||||
|
||||
await page.locator(`[data-name="dash-${type}"]`).click()
|
||||
|
||||
await page.locator('.art-setting-panel.art-current .art-setting-item-left-text').filter({ hasText: new RegExp(`^${text}$`) }).click()
|
||||
}
|
||||
|
||||
const expected = type === 'quality'
|
||||
? [['auto', false], plugin === 'published' ? ['qualityIndex', 'video', 1] : ['representationId', 'video', 'high']]
|
||||
: [['track', 'fr', true]]
|
||||
|
||||
await expect.poll(() => page.evaluate(() => window.dashCalls)).toEqual(expected)
|
||||
|
||||
await expect(page.locator(`.art-control-dash-${type} .art-selector-value`)).toHaveText(text)
|
||||
|
||||
expect(await page.evaluate(type => window.art.setting.find(`dash-${type}`).tooltip, type)).toBe(text)
|
||||
|
||||
await testInfo.attach('controlled-dash-selection', { contentType: 'image/png', body: await page.screenshot() })
|
||||
|
||||
await page.locator('#play').click()
|
||||
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.2)
|
||||
|
||||
expect(await page.evaluate(() => window.art.video.error)).toBeNull()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Controlled plugin contracts use Node's built-in runner.
|
||||
import test from 'node:test'
|
||||
import { bothMenus, dashHost, dashImplementations } from './helpers/dash-control.js'
|
||||
|
||||
const implementations = await dashImplementations()
|
||||
const abr = video => ({ streaming: { abr: { autoSwitchBitrate: { video } } } })
|
||||
for (const { name, factory, sdk } of implementations) {
|
||||
test(`${name}: optional factory, deferred SDK binding and synchronous update shape`, () => {
|
||||
const host = dashHost(sdk)
|
||||
delete host.art.dash
|
||||
const result = factory()(host.art)
|
||||
assert.deepEqual(Object.keys(result).sort(), ['name', 'update'])
|
||||
assert.equal(result.name, 'artplayerPluginDashControl')
|
||||
assert.equal(host.calls.length, 0)
|
||||
host.art.dash = host.dash
|
||||
assert.equal(result.update(), undefined)
|
||||
assert.equal(host.controls.size, 0)
|
||||
assert.equal(host.settings.size, 0)
|
||||
})
|
||||
test(`${name}: ready/restart render named surfaces and preserve default Auto/audio behavior`, () => {
|
||||
const host = dashHost(sdk)
|
||||
factory(bothMenus())(host.art)
|
||||
assert.equal(host.controls.size, 0)
|
||||
host.art.emit('ready')
|
||||
assert.deepEqual([...host.controls.keys()], ['dash-quality', 'dash-audio'])
|
||||
assert.deepEqual([...host.settings.keys()], ['dash-quality', 'dash-audio'])
|
||||
const quality = host.controls.get('dash-quality')
|
||||
const audio = host.controls.get('dash-audio')
|
||||
assert.equal(quality.html, 'Auto')
|
||||
assert.deepEqual(Array.from(quality.selector, item => item.html), ['1080p', '720p', '360p', 'Auto'])
|
||||
assert.deepEqual(Array.from(quality.selector).filter(item => item.default).map(item => item.value), ['auto'])
|
||||
assert.equal(audio.html, 'en')
|
||||
assert.deepEqual(Array.from(audio.selector, item => item.value), host.state.tracks)
|
||||
assert.equal(audio.selector[0].default, true)
|
||||
assert.equal(audio.selector.length, 2, 'Audio has no synthetic Auto item')
|
||||
for (const key of ['dash-quality', 'dash-audio']) {
|
||||
assert.equal(host.controls.get(key).position, 'right')
|
||||
assert.equal(host.controls.get(key).style.padding, '0 10px')
|
||||
assert.equal(host.settings.get(key).width, 200)
|
||||
assert.match(host.settings.get(key).icon, /<svg/)
|
||||
}
|
||||
host.calls.length = 0
|
||||
host.art.emit('restart', 'new.mpd')
|
||||
assert.deepEqual(host.calls.map(call => call[0]), ['controls.update', 'setting.update', 'controls.update', 'setting.update'])
|
||||
})
|
||||
test(`${name}: selectors preserve SDK/check identities, notice order and sync return values`, () => {
|
||||
for (const surface of ['controls', 'settings']) {
|
||||
const host = dashHost(sdk)
|
||||
factory(bothMenus())(host.art).update()
|
||||
const menu = host[surface].get('dash-quality')
|
||||
const item = menu.selector[0]
|
||||
host.calls.length = 0
|
||||
assert.equal(menu.onSelect(item), '1080p')
|
||||
assert.deepEqual(host.calls, [
|
||||
['updateSettings', abr(false)],
|
||||
sdk === 4 ? ['setQualityFor', 'video', 2] : ['setRepresentationForTypeById', 'video', 'top'],
|
||||
['notice', 'Quality: 1080p'],
|
||||
['controls.check', item],
|
||||
['setting.check', item],
|
||||
])
|
||||
assert.equal(host.state.settings.streaming.abr.autoSwitchBitrate.audio, false)
|
||||
assert.equal(host.state.settings.streaming.abr.minBitrate.video, -1)
|
||||
host.calls.length = 0
|
||||
const auto = menu.selector.at(-1)
|
||||
assert.equal(menu.onSelect(auto), 'Auto')
|
||||
assert.deepEqual(host.calls, [['updateSettings', abr(true)], ['notice', 'Quality: Auto'], ['controls.check', auto], ['setting.check', auto]])
|
||||
const audio = host[surface].get('dash-audio')
|
||||
const track = audio.selector[1]
|
||||
host.calls.length = 0
|
||||
assert.equal(audio.onSelect(track), 'fr')
|
||||
assert.equal(host.state.currentTrack, host.state.tracks[1])
|
||||
assert.deepEqual(host.calls, [['setCurrentTrack', host.state.tracks[1]], ['notice', 'Audio: fr'], ['controls.check', track], ['setting.check', track]])
|
||||
}
|
||||
})
|
||||
test(`${name}: getName sees original objects, one argument and no receiver binding`, () => {
|
||||
const host = dashHost(sdk)
|
||||
host.state.settings.streaming.abr.autoSwitchBitrate.video = false
|
||||
const qualityCalls = []
|
||||
const audioCalls = []
|
||||
const options = bothMenus()
|
||||
options.quality.getName = function (...args) {
|
||||
qualityCalls.push([this, args])
|
||||
return `Q${args[0].height}`
|
||||
}
|
||||
options.audio.getName = function (...args) {
|
||||
audioCalls.push([this, args])
|
||||
return `A${args[0].lang}`
|
||||
}
|
||||
factory(options)(host.art).update()
|
||||
assert.deepEqual(qualityCalls.map(([receiver, args]) => [receiver, args.length, args[0]]), [host.state.levels[1], ...host.state.levels].map(item => [undefined, 1, item]))
|
||||
assert.deepEqual(audioCalls.map(([receiver, args]) => [receiver, args.length, args[0]]), [host.state.tracks[0], ...host.state.tracks].map(item => [undefined, 1, item]))
|
||||
assert.equal(host.controls.get('dash-quality').html, 'Q720')
|
||||
assert.equal(host.controls.get('dash-audio').html, 'Aen')
|
||||
})
|
||||
test(`${name}: labels deduplicate without mutating caller SDK arrays`, () => {
|
||||
const host = dashHost(sdk)
|
||||
host.state.levels[1].height = 360
|
||||
host.state.tracks[1].lang = 'en'
|
||||
const levels = [...host.state.levels]
|
||||
const tracks = [...host.state.tracks]
|
||||
const before = JSON.stringify({ levels, tracks })
|
||||
factory(bothMenus())(host.art).update()
|
||||
assert.deepEqual(Array.from(host.controls.get('dash-quality').selector, item => item.html), ['1080p', '360p', 'Auto'])
|
||||
assert.equal(host.controls.get('dash-audio').selector.length, 1)
|
||||
assert.equal(host.controls.get('dash-audio').selector[0].value, tracks[0])
|
||||
assert.equal(JSON.stringify({ levels: host.state.levels, tracks: host.state.tracks }), before)
|
||||
assert.deepEqual(host.state.levels, levels)
|
||||
})
|
||||
test(`${name}: explicit quality keys survive noncontiguous SDK metadata`, () => {
|
||||
const host = dashHost(sdk)
|
||||
host.state.levels.forEach((level, index) => {
|
||||
level[sdk === 4 ? 'qualityIndex' : 'absoluteIndex'] = 10 + index * 20
|
||||
})
|
||||
factory(bothMenus())(host.art).update()
|
||||
const menu = host.controls.get('dash-quality')
|
||||
const item = menu.selector.find(item => item.html === '720p')
|
||||
host.calls.length = 0
|
||||
menu.onSelect(item)
|
||||
assert.deepEqual(host.calls[1], sdk === 4 ? ['setQualityFor', 'video', 30] : ['setRepresentationForTypeById', 'video', 'high'])
|
||||
})
|
||||
test(`${name}: empty topology has no UI and later ready data can render`, () => {
|
||||
const host = dashHost(sdk)
|
||||
const { levels, tracks } = host.state
|
||||
host.state.levels = []
|
||||
host.state.tracks = null
|
||||
const plugin = factory(bothMenus())(host.art)
|
||||
plugin.update()
|
||||
assert.equal(host.calls.length, 0)
|
||||
host.state.levels = levels
|
||||
host.state.tracks = tracks
|
||||
host.art.emit('ready')
|
||||
assert.equal(host.controls.size, 2)
|
||||
})
|
||||
test(`${name}: truthy label defaults and per-surface enablement remain unchanged`, () => {
|
||||
const host = dashHost(sdk)
|
||||
factory({ quality: { control: true, title: '', auto: '' }, audio: { setting: true, title: '' } })(host.art).update()
|
||||
assert.deepEqual([...host.controls.keys()], ['dash-quality'])
|
||||
assert.deepEqual([...host.settings.keys()], ['dash-audio'])
|
||||
assert.equal(host.controls.get('dash-quality').html, 'Auto')
|
||||
assert.equal(host.settings.get('dash-audio').html, 'Audio')
|
||||
host.calls.length = 0
|
||||
const audio = host.settings.get('dash-audio')
|
||||
audio.onSelect(audio.selector[1])
|
||||
assert.equal(host.calls.some(call => call[0] === 'controls.check'), false)
|
||||
})
|
||||
test(`${name}: missing and wrong media ownership fails synchronously`, () => {
|
||||
const host = dashHost(sdk)
|
||||
const plugin = factory(bothMenus())(host.art)
|
||||
host.state.video = {}
|
||||
assert.throws(plugin.update, { message: 'Cannot find instance of DASH from "art.dash"' })
|
||||
assert.equal(host.controls.size, 0)
|
||||
delete host.art.dash
|
||||
assert.throws(plugin.update, { name: 'TypeError' })
|
||||
host.art.dash = host.dash
|
||||
host.state.video = host.video
|
||||
plugin.update()
|
||||
assert.equal(host.controls.size, 2)
|
||||
})
|
||||
test(`${name}: caller getName and SDK setter failures preserve error identity`, () => {
|
||||
const host = dashHost(sdk)
|
||||
const failure = new Error('caller failure')
|
||||
const options = bothMenus()
|
||||
options.quality.getName = () => {
|
||||
throw failure
|
||||
}
|
||||
const plugin = factory(options)(host.art)
|
||||
assert.throws(plugin.update, error => error === failure)
|
||||
assert.equal(host.controls.size, 0)
|
||||
delete options.quality.getName
|
||||
plugin.update()
|
||||
host.dash[sdk === 4 ? 'setQualityFor' : 'setRepresentationForTypeById'] = () => {
|
||||
throw failure
|
||||
}
|
||||
host.calls.length = 0
|
||||
const menu = host.controls.get('dash-quality')
|
||||
assert.throws(() => menu.onSelect(menu.selector[0]), error => error === failure)
|
||||
assert.deepEqual(host.calls, [['updateSettings', abr(false)]])
|
||||
})
|
||||
test(`${name}: SDK replacement and multiple hosts retain independent selections`, () => {
|
||||
const left = dashHost(sdk)
|
||||
const right = dashHost(sdk)
|
||||
factory(bothMenus())(left.art).update()
|
||||
factory(bothMenus())(right.art).update()
|
||||
right.state.video = left.video
|
||||
left.art.dash = right.dash
|
||||
left.art.emit('restart')
|
||||
left.calls.length = 0
|
||||
right.calls.length = 0
|
||||
const menu = left.controls.get('dash-audio')
|
||||
menu.onSelect(menu.selector[1])
|
||||
assert.equal(right.state.currentTrack, right.state.tracks[1])
|
||||
assert.equal(left.state.currentTrack, left.state.tracks[0])
|
||||
assert.equal(left.calls.some(call => call[0] === 'setCurrentTrack'), false)
|
||||
assert.equal(right.calls[0][0], 'setCurrentTrack')
|
||||
})
|
||||
}
|
||||
// These historical factories stay frozen when the live source is repaired.
|
||||
for (const { name, factory, sdk } of implementations.filter(item => item.name.startsWith('published') || item.name === 'pre-refactor-v5')) {
|
||||
test(`${name} observation DASH-LIFE-01: empty/disabled surfaces remain registered`, () => {
|
||||
const host = dashHost(sdk)
|
||||
const options = bothMenus()
|
||||
const plugin = factory(options)(host.art)
|
||||
plugin.update()
|
||||
const previous = host.controls.get('dash-quality')
|
||||
host.state.levels = []
|
||||
host.state.tracks = []
|
||||
plugin.update()
|
||||
assert.equal(host.controls.get('dash-quality'), previous)
|
||||
options.quality.control = options.quality.setting = false
|
||||
options.audio.control = options.audio.setting = false
|
||||
plugin.update()
|
||||
assert.equal(host.controls.size, 2)
|
||||
assert.equal(host.settings.size, 2)
|
||||
})
|
||||
test(`${name} observation DASH-LIFE-01: retained callbacks outlive destruction and SDK replacement`, () => {
|
||||
const host = dashHost(sdk)
|
||||
const plugin = factory(bothMenus())(host.art)
|
||||
plugin.update()
|
||||
const menu = host.controls.get('dash-quality')
|
||||
const next = dashHost(sdk)
|
||||
next.state.video = host.video
|
||||
host.art.dash = next.dash
|
||||
plugin.update()
|
||||
host.art.destroy()
|
||||
host.calls.length = 0
|
||||
menu.onSelect(menu.selector[0])
|
||||
assert.equal(host.calls[1][0], sdk === 4 ? 'setQualityFor' : 'setRepresentationForTypeById')
|
||||
assert.equal(host.listeners.get('ready').size, 1)
|
||||
assert.equal(host.listeners.get('restart').size, 1)
|
||||
host.calls.length = 0
|
||||
plugin.update()
|
||||
assert.equal(host.calls.filter(call => call[0].endsWith('.update')).length, 4)
|
||||
assert.equal(host.calls.some(call => call[0] === 'dash.destroy'), false)
|
||||
})
|
||||
test(`${name} observation DASH-STATE-01: cloned current track and duplicate current label lose highlighting`, () => {
|
||||
const host = dashHost(sdk)
|
||||
host.state.currentTrack = { ...host.state.tracks[1] }
|
||||
host.state.settings.streaming.abr.autoSwitchBitrate.video = false
|
||||
host.state.levels[1].height = 360
|
||||
factory(bothMenus())(host.art).update()
|
||||
assert.equal(host.controls.get('dash-audio').html, 'fr')
|
||||
assert.equal(Array.from(host.controls.get('dash-audio').selector).filter(item => item.default).length, 0)
|
||||
assert.equal(host.controls.get('dash-quality').html, '360p')
|
||||
assert.equal(Array.from(host.controls.get('dash-quality').selector).filter(item => item.default).length, 0)
|
||||
})
|
||||
}
|
||||
test('pre-refactor v5 observation: numeric zero representation ID loses manual highlighting', () => {
|
||||
const { factory } = implementations.find(item => item.name === 'pre-refactor-v5')
|
||||
const host = dashHost(5)
|
||||
host.state.levels[1].id = 0
|
||||
host.state.settings.streaming.abr.autoSwitchBitrate.video = false
|
||||
factory(bothMenus())(host.art).update()
|
||||
const menu = host.controls.get('dash-quality')
|
||||
assert.equal(menu.html, '720p')
|
||||
assert.equal(Array.from(menu.selector).filter(item => item.default).length, 0)
|
||||
})
|
||||
test('frozen SDK generations demonstrate the two incompatible quality method surfaces', () => {
|
||||
const published = implementations.find(item => item.name === 'published-main').factory
|
||||
const historical = implementations.find(item => item.name === 'pre-refactor-v5').factory
|
||||
assert.throws(() => published(bothMenus())(dashHost(5).art).update(), /getBitrateInfoListFor/)
|
||||
assert.throws(() => historical(bothMenus())(dashHost(4).art).update(), /getRepresentationsByType/)
|
||||
})
|
||||
@@ -0,0 +1,194 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import vm from 'node:vm'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { loadPackage } from './load.js'
|
||||
|
||||
async function factoryFromBytes(bytes, module) {
|
||||
if (module)
|
||||
return (await import(`data:text/javascript;base64,${Buffer.from(bytes).toString('base64')}`)).default
|
||||
const context = { module: { exports: {} } }
|
||||
context.exports = context.module.exports
|
||||
vm.runInNewContext(bytes.toString(), context, { timeout: 5000 })
|
||||
assert.equal(typeof context.module.exports, 'function')
|
||||
return context.module.exports
|
||||
}
|
||||
export async function dashImplementations() {
|
||||
const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/dash-control-release.json', import.meta.url)))
|
||||
const { release } = baseline
|
||||
const archive = await ensureArchive(release)
|
||||
const implementations = [{ name: 'source', sdk: 5, factory: (await loadPackage(release.name)).default }]
|
||||
const historicalFile = `packages/${release.name}/dist/artplayer-plugin-dash-control.js`
|
||||
const historical = execFileSync('git', ['show', `${baseline.sourceCommit}:${historicalFile}`])
|
||||
assert.equal(hash(historical.toString().replace(/\r\n/g, '\n')), baseline.source[historicalFile])
|
||||
implementations.push({ name: 'pre-refactor-v5', sdk: 5, factory: await factoryFromBytes(historical, false) })
|
||||
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])
|
||||
implementations.push({ name: `published-${format}`, sdk: 4, factory: await factoryFromBytes(bytes, format === 'module') })
|
||||
}
|
||||
for (const filename of (process.env.ARTPLAYER_TEST_DASH || '').split(path.delimiter).filter(Boolean)) {
|
||||
const bytes = fs.readFileSync(filename)
|
||||
implementations.push({ name: `artifact-${path.basename(filename)}`, sdk: 5, factory: await factoryFromBytes(bytes, filename.endsWith('.mjs')) })
|
||||
}
|
||||
return implementations
|
||||
}
|
||||
// A controlled SDK surface and registry. It does not decode media or simulate ABR.
|
||||
export function dashHost(version = 5) {
|
||||
const calls = []
|
||||
const controls = new Map()
|
||||
const settings = new Map()
|
||||
const listeners = new Map()
|
||||
const video = {}
|
||||
const levels = [
|
||||
{ id: 'low', qualityIndex: 0, height: 360, bitrateInKbit: 400 },
|
||||
{ id: 'high', qualityIndex: 1, height: 720, bitrateInKbit: 1200 },
|
||||
{ id: 'top', qualityIndex: 2, height: 1080, bitrateInKbit: 2400 },
|
||||
]
|
||||
for (const level of levels) {
|
||||
if (version === 4)
|
||||
delete level.id
|
||||
else
|
||||
delete level.qualityIndex
|
||||
}
|
||||
const tracks = [{ id: 'en', lang: 'en', index: 0 }, { id: 'fr', lang: 'fr', index: 1 }]
|
||||
const state = {
|
||||
video,
|
||||
levels,
|
||||
tracks,
|
||||
currentRepresentation: levels[1],
|
||||
currentQuality: 1,
|
||||
currentTrack: tracks[0],
|
||||
settings: { streaming: { abr: { autoSwitchBitrate: { video: true, audio: false }, minBitrate: { video: -1 } } } },
|
||||
}
|
||||
const art = {
|
||||
template: { $video: video },
|
||||
isDestroy: false,
|
||||
constructor: { utils: { errorHandle(condition, message) {
|
||||
if (!condition)
|
||||
throw new Error(message)
|
||||
} } },
|
||||
notice: {},
|
||||
on(name, callback) {
|
||||
if (!listeners.has(name))
|
||||
listeners.set(name, new Set())
|
||||
listeners.get(name).add(callback)
|
||||
return art
|
||||
},
|
||||
off(name, callback) {
|
||||
listeners.get(name)?.delete(callback)
|
||||
return art
|
||||
},
|
||||
emit(name, ...args) {
|
||||
for (const callback of [...(listeners.get(name) || [])])
|
||||
callback(...args)
|
||||
return art
|
||||
},
|
||||
destroy() {
|
||||
art.isDestroy = true
|
||||
art.emit('destroy')
|
||||
},
|
||||
}
|
||||
let notice = ''
|
||||
Object.defineProperty(art.notice, 'show', {
|
||||
get: () => notice,
|
||||
set(value) {
|
||||
notice = value
|
||||
calls.push(['notice', value])
|
||||
},
|
||||
})
|
||||
for (const [name, entries] of [['controls', controls], ['setting', settings]]) {
|
||||
art[name] = {
|
||||
update(option) {
|
||||
entries.set(option.name, option)
|
||||
calls.push([`${name}.update`, option.name])
|
||||
},
|
||||
remove(key) {
|
||||
entries.delete(key)
|
||||
calls.push([`${name}.remove`, key])
|
||||
},
|
||||
check(item) {
|
||||
calls.push([`${name}.check`, item])
|
||||
},
|
||||
}
|
||||
}
|
||||
const dash = {
|
||||
getVideoElement() {
|
||||
assert.equal(this, dash)
|
||||
return state.video
|
||||
},
|
||||
getSettings() {
|
||||
assert.equal(this, dash)
|
||||
return state.settings
|
||||
},
|
||||
updateSettings(update) {
|
||||
assert.equal(this, dash)
|
||||
calls.push(['updateSettings', JSON.parse(JSON.stringify(update))])
|
||||
Object.assign(state.settings.streaming.abr.autoSwitchBitrate, update.streaming.abr.autoSwitchBitrate)
|
||||
},
|
||||
getTracksFor(type) {
|
||||
assert.equal(this, dash)
|
||||
assert.equal(type, 'audio')
|
||||
return state.tracks
|
||||
},
|
||||
getCurrentTrackFor(type) {
|
||||
assert.equal(this, dash)
|
||||
assert.equal(type, 'audio')
|
||||
return state.currentTrack
|
||||
},
|
||||
setCurrentTrack(track) {
|
||||
assert.equal(this, dash)
|
||||
state.currentTrack = track
|
||||
calls.push(['setCurrentTrack', track])
|
||||
},
|
||||
destroy() {
|
||||
calls.push(['dash.destroy'])
|
||||
},
|
||||
}
|
||||
if (version === 4) {
|
||||
Object.assign(dash, {
|
||||
getBitrateInfoListFor(type) {
|
||||
assert.equal(this, dash)
|
||||
assert.equal(type, 'video')
|
||||
return state.levels
|
||||
},
|
||||
getQualityFor(type) {
|
||||
assert.equal(this, dash)
|
||||
assert.equal(type, 'video')
|
||||
return state.currentQuality
|
||||
},
|
||||
setQualityFor(type, index) {
|
||||
assert.equal(this, dash)
|
||||
state.currentQuality = index
|
||||
calls.push(['setQualityFor', type, index])
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
Object.assign(dash, {
|
||||
getRepresentationsByType(type) {
|
||||
assert.equal(this, dash)
|
||||
assert.equal(type, 'video')
|
||||
return state.levels
|
||||
},
|
||||
getCurrentRepresentationForType(type) {
|
||||
assert.equal(this, dash)
|
||||
assert.equal(type, 'video')
|
||||
return state.currentRepresentation
|
||||
},
|
||||
setRepresentationForTypeById(type, id) {
|
||||
assert.equal(this, dash)
|
||||
state.currentRepresentation = state.levels?.find(level => level.id === id)
|
||||
calls.push(['setRepresentationForTypeById', type, id])
|
||||
},
|
||||
})
|
||||
}
|
||||
art.dash = dash
|
||||
return { art, dash, state, calls, controls, settings, listeners, video }
|
||||
}
|
||||
export const bothMenus = () => ({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })
|
||||
Reference in new issue
Block a user