IPTVnator Web 播放器共享控制设置(Shared Controls)的设计与实现
【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator
导读
IPTVnator 在app-player-controls共享控制层之上,为 HTML5、Video.js、ArtPlayer 三种 Web 播放引擎引入了统一的控件体系,并通过一个持久化的实验性设置项让用户自行选择是否启用。本文以 web-player-shared-controls-setting 设计文档 为核心骨架,结合仓库内SettingsStore、设置表单、播放器宿主组件与测试用例的源码实现,完整还原该设置从“设计契约 → 表单 UI → 会话级不可变快照”的落地链路。读完本文,你将理解:为什么该设置只面向 Web 播放器而不影响 Embedded MPV 与外部播放器、设置如何在无需重启应用的前提下作用于下一次播放会话、以及控件模式在单个会话生命周期内保持不可变的底层原因。
背景:四条播放路径与共享控制层
在讨论设置项之前,需要先理解 IPTVnator 当前的播放架构。共享的app-player-controls呈现层已经集成了四条播放路径:
| 播放路径 | 控件策略 |
|---|---|
| Embedded MPV(frame-copy 帧拷贝) | 无条件使用app-player-controls(视频渲染进 DOM canvas,必须使用 DOM 控件) |
| Embedded MPV(native-view 原生视图) | 使用合成器安全的 legacy 控件坞(DOM 控件无法可靠叠放在原生表面之上) |
| HTML5(hls.js) | 可选用共享控件 |
| Video.js | 可选用共享控件 |
| ArtPlayer | 可选用共享控件 |
其中三个 Web 引擎共享同一个名为WEB_PLAYER_SHARED_CONTROLS的注入令牌(InjectionToken),该令牌定义在 web-player-controls.flag.ts。设计文档描述的初始状态是:该令牌从编译期常量WEB_PLAYER_SHARED_CONTROLS_ENABLED = false解析,意味着要启用共享控件必须先改代码、重新构建并重启应用,无法进行低成本的灰度验证。
设计文档进一步指出一个关键事实:Settings 路由不会保持一个活跃的播放器挂载。因此设置变更不需要在运行中的引擎上即时生效,也不需要保留播放位置——只要在“下一次播放会话创建”时应用保存的选择即可。这正是整个运行时数据流设计的出发点。
设计目标与非目标
Goals(目标)
- 在 Playback 区段增加一个实验性复选框,为 HTML5、Video.js、ArtPlayer 开启共享控件;
- 将选择持久化到权威的
SettingsStore; - 让保存的选择在不重启、不重载应用的情况下作用于下一次 Web 播放会话;
- 保持控件模式在单个播放器会话生命周期内不可变;
- 保持默认关闭(default-off)行为,并与已存在的旧版存储设置兼容;
- 保持 Embedded MPV 与外部播放器的行为完全不变。
Non-goals(非目标)
- 不将共享 Web 控件设为默认;
- 不在 legacy 与 shared 控件之间动态切换已激活的播放器;
- 不为 HTML5、Video.js、ArtPlayer 分别提供独立偏好;
- 不让 Embedded MPV native-view 使用 DOM 覆盖层;
- 不允许 frame-copy Embedded MPV 回退到 legacy 控件坞;
- 不控制外部 MPV/VLC 进程的原生 UI。
这组“非目标”很重要:它明确划定了该设置的职责边界,避免因过度设计破坏已验证的播放引擎行为。
Embedded MPV 语义:为什么这是“Web 播放器专属”设置
新复选框被刻意设计为 Web 播放器专属,原因在于 Embedded MPV 的两条渲染路径对控件形态有硬性约束:
- Frame-copy 路径:视频被上传到渲染进程 canvas,
app-player-controls是其必需组成部分。若再给 Embedded MPV 一个共享控件开关,只会重复一个本来就必需的功能,属于无效选项; - Native-view 路径:视频托管在平台原生表面中,DOM 控件无法可靠叠放其上,因此合成器安全的 legacy 控件坞仍是必需的。
已有的embeddedMpvFrameCopy设置继续负责选择渲染引擎,并且仍然需要重启应用才生效——它与新的 Web 播放器控件偏好相互独立、互不干扰。外部 MPV/VLC 进程则始终拥有自己的控件 UI。
这一语义从实现上也可以验证:WebPlayerViewComponent为WEB_PLAYER_SHARED_CONTROLS提供组件级 provider,但只有三个 Web 引擎组件会注入该令牌;Embedded MPV 组件不注入令牌,因此其控件形态完全由引擎类型驱动(frame-copy用共享控件、native-view 用 legacy 坞)。
候选方案评估
设计文档对比了三种方案:
1. 条件式全局 Web 播放器偏好(被选中)
一个设置统一控制 HTML5、Video.js、ArtPlayer 三者,复选框仅在选中其中之一时显示。优点:与现有单一 rollout 令牌一致、设置 UI 始终与当前选中的播放器相关、避免暗示该选项会影响 Embedded MPV 或外部播放器。
2. 始终可见的“在可用处使用共享控件”偏好(被否决)
对 frame-copy Embedded MPV 是无效操作、对 native-view 不支持、对外部播放器超出应用范围,条件语义难以用标签解释清楚。
3. 为每个播放引擎提供独立偏好(被否决)
三个 Web 集成刻意共享同一个 rollout 契约,逐引擎偏好会徒增设置项与测试组合,且当前没有产品需求支撑。
结论是:一个条件显示的、面向三类 Web 引擎的单一偏好是语义最清晰、实现成本最低的方案。
设置契约:SettingsStore 与可选布尔字段
接口定义
在共享设置接口中新增一个可选布尔字段:
/** * Use IPTVnator's shared controls in HTML5, Video.js, and ArtPlayer. * Missing values remain off for compatibility with older saved settings. */ webPlayerSharedControls?: boolean;规范要求:
- 规范默认值为
false; - 加载不含该字段的旧设置对象时,必须规范化(normalize)为
false; - 保存设置时,必须将完整解析后的值写回存储。
当前仓库中该字段的规范化与序列化实现在 settings-store.service.ts:getSettings()中以store.webPlayerSharedControls?.() !== false输出完整布尔值,确保存储中始终写入已解析的完整状态。对应的契约测试位于 settings-store.service.spec.ts,覆盖了缺失字段、布尔真值、畸形字符串(如'false')与保存序列化等场景。
设计演进说明
需要特别指出:设计文档撰写时规划的规范默认值是false(default-off,编译期常量WEB_PLAYER_SHARED_CONTROLS_ENABLED = false)。而当前仓库的实现已经落地并进一步演进为default-ON(默认开启,复选框成为“回到厂商原生控件”的退出开关):
- settings-store.service.ts 的
DEFAULT_SETTINGS中为webPlayerSharedControls: true; - web-player-controls.flag.ts 中
WEB_PLAYER_SHARED_CONTROLS_ENABLED = true,注释明确“默认开启,用户通过Settings.webPlayerSharedControls选择退出”; - settings-form.utils.ts 表单默认
true,序列化时value.webPlayerSharedControls ?? true。
这一演进同样反映在权威文档中:player-controls-contract.md 记录了 “Settings.webPlayerSharedControlsis default-ON: an absent stored value means…”,AGENTS.md也同步说明“PersistedSettings.webPlayerSharedControlsis default-ON”。读者在阅读或贡献代码时,应以当前仓库的 default-ON 行为为准;设计文档中的 default-off 属于方案定稿时的初始契约,两者在“规范化、持久化、会话快照”等机制上完全一致。
表单契约(renderer-only 状态)
设置表单新增webPlayerSharedControls控件,默认false(按设计文档;当前实现为true)。表单的水合(hydration)、序列化、重置行为、备份/导出以及既有 Save 工作流,全部继续使用完整的Settings对象,无需新增专用代码路径。这是纯渲染进程状态:Electron 主进程行为与 preload IPC 都不需要新增命令。
运行时数据流:从设置到下一次会话
设计文档给出了完整的运行时解析流程:
SettingsStore加载webPlayerSharedControls,缺失时默认为false;- 设置页通过既有 Save 动作更新并持久化该值;
- 离开设置页时该路由被销毁——变更期间没有活跃的播放器;
- 下一次
WebPlayerViewComponent从SettingsStore解析一个组件作用域的布尔快照; - HTML5、Video.js 或 ArtPlayer 通过
WEB_PLAYER_SHARED_CONTROLS接收该快照,并恰好构造一套控件系统; - 在该播放器宿主被销毁、新播放会话创建之前,快照不会改变。
不可变快照的底层原理
“不可变”不是实现细节,而是 Video.js 与 ArtPlayer 的构造期约束:厂商控件、快捷键、手势、插件、源桥(source bridge)与共享适配器必须原子化配置。如果用一个响应式布尔值只翻转模板、而引擎仍按另一种模式配置,就会产生控件系统与引擎状态不一致的撕裂状态。
因此实现采用Angular 组件级 provider 工厂。核心代码在 web-player-shared-controls.ts:
export function resolveWebPlayerSharedControls(): boolean { const storedValue = inject(SettingsStore).webPlayerSharedControls?.(); return typeof storedValue === 'boolean' ? storedValue : WEB_PLAYER_SHARED_CONTROLS_ENABLED; }WebPlayerViewComponent在@Component.providers中声明该工厂(见 web-player-view.component.ts)。Angular 会在WebPlayerViewComponent的元素注入器中创建并缓存这个原始布尔值,HTML5、Video.js、ArtPlayer 三个子组件因此各自接收到一个不可变的值,用于它们共享的构造期分支判断——这恰好满足了“一个会话只渲染/初始化一套控件系统”的约束。而 Embedded MPV 不注入该令牌,继续由引擎类型驱动。
配套测试 web-player-view.component.shared-controls.spec.ts 验证了两个关键性质:修改SettingsStore信号只影响下一次宿主创建;已存在的宿主在其存活期间令牌值保持不变。
设置 UI:Playback 区段的条件复选框
设计文档对 UI 的要求是:在 Playback 区段增加一个标准setting-item,复用既有 Material 复选框与布局样式,不引入新的颜色、间距规则或一次性组件。
- 标签含义:Use unified controls for web players (experimental)(为 Web 播放器使用统一控件,实验性);
- 描述含义:在 HTML5、Video.js 和 ArtPlayer 中使用 IPTVnator 的共享控件;
- 可见性:仅当当前选中的播放器为
videojs、html5或artplayer时显示; - 隐藏场景:Embedded MPV、外部 MPV、VLC 下隐藏;
- 为行与复选框提供稳定的测试选择器(
data-test-id="web-player-shared-controls-setting"与data-test-id="web-player-shared-controls-toggle"); - 为每个 locale 文件补充相同的标签与描述键,保持 i18n 键集合对齐。
模板实现
当前实现位于 settings-playback-section.component.html,条件渲染结构与设计文档完全一致:
@if (isWebPlayerSelected()) { <div class="setting-item" >pnpm nx build shared-interfaces --skip-nx-cache pnpm nx test services --skip-nx-cache --runInBand \ --testPathPatterns=settings-store.service.spec2. 设置表单与 UI
- 表单创建与序列化包含新字段;
- 复选框从存储设置水合,并包含在 Save 载荷中;
- 行为为 HTML5、Video.js、ArtPlayer 时可见,Embedded MPV、外部 MPV、VLC 时隐藏;
- 所有 locale 的翻译键对齐。
pnpm nx test web --skip-nx-cache --runInBand \ --testPathPatterns='settings-playback-section.component.spec|settings.component.spec'当前仓库的 settings-playback-section.component.spec.ts 与 settings.component.form.spec.ts 即覆盖了上述水合/保存/可见性断言。
3. 播放宿主
- 新建的 HTML5、Video.js、ArtPlayer 会话收到保存的布尔快照;
false保留厂商/原生 UI,true挂载共享控件路径;- Embedded MPV 选择忽略该 Web 偏好;
- 单个会话永远不会同时渲染/初始化两套控件系统。
pnpm nx test ui-playback --skip-nx-cache --runInBand \ --testPathPatterns=web-player-view.component.shared-controls pnpm nx test ui-playback --skip-nx-cache4. E2E 验证
- Web(浏览器):
apps/web-e2e/src/settings.e2e.ts中验证保存后刷新页面复选框保持勾选; - Electron 桌面端:
apps/electron-backend-e2e/src/settings.e2e.ts中验证跨应用重启持久化,以及“保存后首个 HTML5 会话渲染app-player-controls且原生video[controls]数量为 0”的端到端冒烟断言。
pnpm nx run web-e2e:e2e-ci--src/settings.e2e.ts pnpm nx run electron-backend-e2e:e2e-ci--src/settings.e2e.ts5. 全量回归阶梯
设计文档要求最终跑通:shared-interfaces构建、services/web/ui-playback测试、相关项目 lint、typecheck:web、i18n:check、web生产构建,以及 web/Electron 两套 settings E2E,全部命令退出码为 0 才视为通过。
文档影响与仓库现状
设计文档要求同步更新以下文档,将“仅编译期默认关闭的 rollout 描述”替换为“持久化的实验性偏好”:
- player-controls-contract.md:权威控件契约,记录
Settings.webPlayerSharedControls的默认值、WEB_PLAYER_SHARED_CONTROLS_ENABLED回退常量与WEB_PLAYER_SHARED_CONTROLS会话快照三者关系; - AGENTS.md 与 CLAUDE.md:保持共享控件描述的同步;
README.md:在 Playback 功能列表中补充“可选统一控件(实验性)”条目;- Embedded MPV 架构文档仅在现有 frame-copy 共享控件与 native-view legacy 坞的区分会产生歧义时才需要澄清。
当前仓库中该功能已完整落地:设置契约(libs/services)、表单与 UI(apps/web/src/app/settings/)、播放宿主快照(libs/ui/playback/src/lib/web-player-view/)、国际化(apps/web/src/assets/i18n/)以及两端 E2E 覆盖均已就位。如果你想实际体验:在 Web 或桌面端打开「设置 → 播放」,将播放器切换为 HTML5、Video.js 或 ArtPlayer 之一,即可看到该实验性复选框;保存后返回播放页面,下一次会话即按保存的选择使用 IPTVnator 的统一控件。
【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考