mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
feat(settings): add shared web player controls toggle (#1198)
* docs(playback): design shared controls setting * docs(playback): plan shared controls setting * feat(settings): persist shared web controls preference * test(settings): harden shared controls normalization coverage * feat(settings): expose shared web controls toggle * fix(settings): label shared controls toggle * feat(playback): resolve shared controls from settings * test(playback): cover shared controls setting * docs(playback): document shared controls preference * fix(playback): await settings before host creation * fix(settings): normalize shared controls updates
This commit is contained in:
1 parent
48e3736460
commit
beb62db314
38 files changed
+1863
-300
No files matched your search
@@ -14,14 +14,15 @@ consumers and includes:
|
||||
- the standalone `app-player-controls` presentation component and its
|
||||
transient-state collaborators;
|
||||
- a generic `WebVideoControlsAdapter` plus small host helpers;
|
||||
- a default-off web rollout token;
|
||||
- a persisted, default-off web-player preference resolved through an immutable
|
||||
per-host rollout token;
|
||||
- the component-scoped `EmbeddedMpvControlsAdapter`;
|
||||
- an `EmbeddedMpvPlayerComponent` host integration for the frame-copy engine;
|
||||
- a feature-flagged `HtmlVideoPlayerComponent` integration backed by
|
||||
- a preference-guarded `HtmlVideoPlayerComponent` integration backed by
|
||||
`WebVideoControlsAdapter` and a player-local engine bridge;
|
||||
- a feature-flagged `VjsPlayerComponent` integration backed by a
|
||||
- a preference-guarded `VjsPlayerComponent` integration backed by a
|
||||
component-scoped `WebVideoControlsAdapter` and Video.js bridge;
|
||||
- a feature-flagged `ArtPlayerComponent` integration backed by a
|
||||
- a preference-guarded `ArtPlayerComponent` integration backed by a
|
||||
component-scoped `WebVideoControlsAdapter`, neutral web-video source bridge,
|
||||
and player-local source/video sessions; and
|
||||
- focused unit/component tests.
|
||||
@@ -36,14 +37,14 @@ When `WEB_PLAYER_SHARED_CONTROLS` is enabled, the built-in HTML5 player mounts
|
||||
the same presentation component over its real player shell and disables the
|
||||
native video controls. Its neutral source bridge supplies HLS/native tracks,
|
||||
corrected MPEG-TS VOD duration, and authoritative live/VOD metadata to the
|
||||
generic web adapter. When the flag is disabled, the native controls and legacy
|
||||
series navigation remain unchanged and the adapter is not attached.
|
||||
generic web adapter. When the host token resolves to false, the native controls
|
||||
and legacy series navigation remain unchanged and the adapter is not attached.
|
||||
|
||||
Video.js consumes the same token and shared presentation atomically. Its bridge
|
||||
binds the adapter to the current Video.js Tech `<video>` and rebinds after
|
||||
`playerreset`, while focused collaborators expose Video.js audio/text tracks
|
||||
and manage raw MPEG-TS playback. With the flag disabled, Video.js keeps its
|
||||
existing skin and legacy series navigation.
|
||||
and manage raw MPEG-TS playback. When the host token resolves to false, Video.js
|
||||
keeps its existing skin and legacy series navigation.
|
||||
|
||||
ArtPlayer is the fourth consumer. Its source session owns HLS, MPEG-TS, native
|
||||
source selection, and delayed `customType` callbacks, while the neutral
|
||||
@@ -56,17 +57,22 @@ event-capture layer over ArtPlayer so shared controls exclusively own surface
|
||||
clicks and double-clicks. Playback diagnostics gate shared interaction and exit
|
||||
only the ArtPlayer shell's own fullscreen. Source replacement and teardown
|
||||
remove exact listeners and engines, and destroyed sessions ignore stale delayed
|
||||
`customType` callbacks. With the flag disabled, the existing ArtPlayer skin,
|
||||
source behavior, and legacy series navigation remain unchanged.
|
||||
`customType` callbacks. When the host token resolves to false, the existing
|
||||
ArtPlayer skin, source behavior, and legacy series navigation remain unchanged.
|
||||
|
||||
`WEB_PLAYER_SHARED_CONTROLS_ENABLED` remains default-off, so none of the three
|
||||
guarded web integrations changes normal runtime behavior.
|
||||
`Settings.webPlayerSharedControls` remains default-off. `WebPlayerViewComponent`
|
||||
snapshots it into `WEB_PLAYER_SHARED_CONTROLS` when a new player host is
|
||||
created, so HTML5, Video.js, and ArtPlayer switch atomically without an
|
||||
application restart. The parent `/workspace` route awaits the initial
|
||||
`SettingsStore` load, including for cold-start direct links to workspace
|
||||
children, before any player host can take this snapshot. Existing sessions
|
||||
never change controls mode in place.
|
||||
|
||||
This rollout is intentionally engine-selective: frame-copy can use normal DOM
|
||||
layering, while the native platform view cannot. The integration also includes
|
||||
a recording coordinator that correlates asynchronous snapshots with the active
|
||||
playback/session owner, serializes toggles, and cancels pending ownership when
|
||||
the session, playback, engine, or component changes.
|
||||
The shared-controls architecture remains engine-selective: frame-copy can use
|
||||
normal DOM layering, while the native platform view cannot. The integration
|
||||
also includes a recording coordinator that correlates asynchronous snapshots
|
||||
with the active playback/session owner, serializes toggles, and cancels pending
|
||||
ownership when the session, playback, engine, or component changes.
|
||||
|
||||
## Why this exists
|
||||
|
||||
@@ -111,10 +117,10 @@ contract does not make a native video surface behave like DOM content.
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────────┐ ┌──────────────────────────────┐
|
||||
│ Flag-guarded web hosts │ │ EmbeddedMpvPlayerComponent │
|
||||
│ Per-host preference snapshot │ │ EmbeddedMpvPlayerComponent │
|
||||
│ HTML5 + Video.js + ArtPlayer │ │ frame-copy: shared controls │
|
||||
│ flag on: shared controls │ │ native-view: legacy dock │
|
||||
│ flag off: existing controls │ └──────────────────────────────┘
|
||||
│ true: shared controls │ │ native-view: legacy dock │
|
||||
│ false: existing controls │ └──────────────────────────────┘
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -122,7 +128,7 @@ The embedded host selects controls from the reported engine before rendering
|
||||
them. It never mounts the shared overlay and legacy dock together.
|
||||
The HTML5, Video.js, and ArtPlayer hosts likewise select their existing or
|
||||
shared controls before rendering and never attach the web adapter while the
|
||||
flag-off path is active.
|
||||
preference-off path is active.
|
||||
|
||||
## The contract
|
||||
|
||||
@@ -375,12 +381,13 @@ attachment/projection helpers. Video.js uses its dedicated bridge directly.
|
||||
HTML5 and ArtPlayer share the neutral source bridge and HLS/native-track
|
||||
collaborators under `web-video-support/`.
|
||||
|
||||
The rollout symbols are:
|
||||
The rollout symbols and setting are:
|
||||
|
||||
| Symbol | Default | Current effect |
|
||||
| ------------------------------------ | ------------: | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `WEB_PLAYER_SHARED_CONTROLS_ENABLED` | `false` | Keeps existing web-player skins active in normal runtime builds. |
|
||||
| `WEB_PLAYER_SHARED_CONTROLS` | default above | Injectable/test-overridable view consumed by HTML5, Video.js, and ArtPlayer to switch atomically between existing and shared UI. |
|
||||
| Symbol / setting | Default | Current effect |
|
||||
| ------------------------------------ | ---------------: | ----------------------------------------------------------------------- |
|
||||
| `Settings.webPlayerSharedControls` | `false` | Persisted experimental opt-in shown for HTML5, Video.js, and ArtPlayer. |
|
||||
| `WEB_PLAYER_SHARED_CONTROLS_ENABLED` | `false` | Default-off fallback for direct component use and focused tests. |
|
||||
| `WEB_PLAYER_SHARED_CONTROLS` | session snapshot | Component-scoped immutable value consumed by the three web engines. |
|
||||
|
||||
With the token enabled, Video.js also disables native controls, Video.js
|
||||
single-click and double-click actions, Video.js hotkeys, and spatial navigation.
|
||||
@@ -403,6 +410,10 @@ semantics, stored volume behavior, and series navigation remain unchanged.
|
||||
The shared contract does not replace either Embedded MPV renderer. The host
|
||||
uses the renderer's reported engine to choose the compatible controls UI.
|
||||
|
||||
The web-player preference does not affect Embedded MPV. Frame-copy always uses
|
||||
the shared DOM controls, while native-view keeps its compositor-safe legacy
|
||||
dock.
|
||||
|
||||
### Frame-copy engine
|
||||
|
||||
The experimental frame-copy engine uploads helper-produced frames to
|
||||
@@ -528,8 +539,8 @@ libs/ui/playback/src/lib/html-video-player/
|
||||
`WebVideoControlsAdapter`. Its bridge/helper filenames re-export the neutral
|
||||
web-video support so existing imports and focused specs remain stable.
|
||||
`HtmlVideoElementSession` separately owns native video-event attachment,
|
||||
persisted volume, start-time/time/ended propagation, and the flag-off post-play
|
||||
caption behavior.
|
||||
persisted volume, start-time/time/ended propagation, and the preference-off
|
||||
post-play caption behavior.
|
||||
|
||||
The guarded Video.js integration lives in:
|
||||
|
||||
@@ -571,11 +582,12 @@ libs/ui/playback/src/lib/art-player/
|
||||
bridge, exact engine/listener cleanup, and a destroyed-session guard for
|
||||
ArtPlayer's delayed `customType` dispatch. `ArtPlayerVideoSession` owns native
|
||||
media errors, readiness, volume persistence, ended/time updates, and exact
|
||||
event cleanup. The setup helper preserves the legacy option set when the flag
|
||||
is off and disables vendor interaction owners when it is on; the component's
|
||||
transparent capture layer blocks ArtPlayer's core surface handlers.
|
||||
event cleanup. The setup helper preserves the legacy option set when the host
|
||||
token resolves to false and disables vendor interaction owners when it resolves
|
||||
to true; the component's transparent capture layer blocks ArtPlayer's core
|
||||
surface handlers.
|
||||
|
||||
Focused specs cover each web engine's flag-off compatibility path,
|
||||
Focused specs cover each web engine's preference-off compatibility path,
|
||||
shared-controls rendering and diagnostic interaction gating, source/element
|
||||
replacement, track-list lifecycle and stable IDs, caption preference and
|
||||
explicit-off behavior, MPEG-TS live/VOD handling and duration projection,
|
||||
|
||||
@@ -0,0 +1,898 @@
|
||||
# Web-Player Shared Controls Setting Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a persisted experimental Playback checkbox that enables the existing shared controls for newly created HTML5, Video.js, and ArtPlayer sessions without restarting IPTVnator.
|
||||
|
||||
**Architecture:** `SettingsStore` owns a default-off `webPlayerSharedControls` boolean and the Settings form exposes it only for the three web engines. `WebPlayerViewComponent` resolves the saved value into a component-scoped, immutable `WEB_PLAYER_SHARED_CONTROLS` snapshot, preserving each engine's construction-time legacy/shared split while leaving Embedded MPV and external players unchanged.
|
||||
|
||||
**Tech Stack:** Angular standalone components and reactive forms, NgRx Signal Store, Angular DI, Angular Material, ngx-translate JSON locales, Jest/TestBed, Playwright, Nx.
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### Settings contract and persistence
|
||||
|
||||
- Modify `libs/shared/interfaces/src/lib/settings.interface.ts`
|
||||
- Declare the optional persisted boolean.
|
||||
- Modify `libs/services/src/lib/settings-store.service.ts`
|
||||
- Add the default, normalize stored input, and include the resolved value in
|
||||
complete settings serialization.
|
||||
- Modify `libs/services/src/lib/settings-store.service.spec.ts`
|
||||
- Cover missing, enabled, malformed, and persisted values.
|
||||
|
||||
### Settings form and presentation
|
||||
|
||||
- Modify `apps/web/src/app/settings/settings-form.utils.ts`
|
||||
- Add the form control and form-to-settings mapping.
|
||||
- Modify `apps/web/src/app/settings/settings-playback-section.component.ts`
|
||||
- Identify whether the selected engine is one of the three web players.
|
||||
- Modify `apps/web/src/app/settings/settings-playback-section.component.html`
|
||||
- Render the conditional Material checkbox.
|
||||
- Modify `apps/web/src/app/settings/settings-playback-section.component.spec.ts`
|
||||
- Cover visibility and checkbox binding.
|
||||
- Modify `apps/web/src/app/settings/settings.component.spec.ts`
|
||||
- Keep the canonical test fixture shape current and verify hydration/save.
|
||||
- Modify all JSON files under `apps/web/src/assets/i18n/`
|
||||
- Add the same two `SETTINGS` keys with localized values.
|
||||
|
||||
### Playback session wiring
|
||||
|
||||
- Modify `libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts`
|
||||
- Provide an immutable, settings-backed controls token per player host.
|
||||
- Modify
|
||||
`libs/ui/playback/src/lib/web-player-view/web-player-view.component.shared-controls.spec.ts`
|
||||
- Verify enabled/disabled resolution and per-host snapshot behavior.
|
||||
|
||||
### End-to-end coverage and documentation
|
||||
|
||||
- Modify `apps/web-e2e/src/settings.e2e.ts`
|
||||
- Verify browser persistence after Save and reload.
|
||||
- Modify `apps/electron-backend-e2e/src/settings.e2e.ts`
|
||||
- Verify desktop persistence across restart and shared controls on the next
|
||||
HTML5 playback session.
|
||||
- Modify `README.md`
|
||||
- Mention the optional unified built-in web-player controls.
|
||||
- Modify `docs/architecture/player-controls-contract.md`
|
||||
- Replace compile-time-only rollout language with the persisted setting and
|
||||
per-session snapshot.
|
||||
- Modify `AGENTS.md` and `CLAUDE.md`
|
||||
- Keep their shared-controls descriptions synchronized.
|
||||
|
||||
## Task 1: Persist the Default-Off Settings Contract
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `libs/shared/interfaces/src/lib/settings.interface.ts`
|
||||
- Modify: `libs/services/src/lib/settings-store.service.ts`
|
||||
- Test: `libs/services/src/lib/settings-store.service.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Write failing store behavior tests**
|
||||
|
||||
Add these cases to `libs/services/src/lib/settings-store.service.spec.ts`.
|
||||
The temporary intersections keep the red test compiling before the shared
|
||||
interface declares the field:
|
||||
|
||||
```ts
|
||||
it('defaults shared web controls to false when the stored field is missing', async () => {
|
||||
const store = injector.get(SettingsStore);
|
||||
|
||||
await store.loadSettings();
|
||||
|
||||
const settings = store.getSettings() as ReturnType<
|
||||
typeof store.getSettings
|
||||
> & {
|
||||
webPlayerSharedControls?: unknown;
|
||||
};
|
||||
expect(settings.webPlayerSharedControls).toBe(false);
|
||||
});
|
||||
|
||||
it('restores an enabled shared web controls preference', async () => {
|
||||
storedSettings = {
|
||||
webPlayerSharedControls: true,
|
||||
} as Partial<Settings> & { webPlayerSharedControls: boolean };
|
||||
const store = injector.get(SettingsStore);
|
||||
|
||||
await store.loadSettings();
|
||||
|
||||
const settings = store.getSettings() as ReturnType<
|
||||
typeof store.getSettings
|
||||
> & {
|
||||
webPlayerSharedControls?: unknown;
|
||||
};
|
||||
expect(settings.webPlayerSharedControls).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes a malformed shared web controls preference to false', async () => {
|
||||
storedSettings = {
|
||||
webPlayerSharedControls: 'enabled',
|
||||
} as unknown as Partial<Settings>;
|
||||
const store = injector.get(SettingsStore);
|
||||
|
||||
await store.loadSettings();
|
||||
|
||||
const settings = store.getSettings() as ReturnType<
|
||||
typeof store.getSettings
|
||||
> & {
|
||||
webPlayerSharedControls?: unknown;
|
||||
};
|
||||
expect(settings.webPlayerSharedControls).toBe(false);
|
||||
});
|
||||
|
||||
it('persists the resolved shared web controls preference', async () => {
|
||||
const store = injector.get(SettingsStore);
|
||||
|
||||
await store.updateSettings({
|
||||
webPlayerSharedControls: true,
|
||||
} as Partial<Settings> & { webPlayerSharedControls: boolean });
|
||||
|
||||
expect(storage.set).toHaveBeenCalledWith(
|
||||
STORE_KEY.Settings,
|
||||
expect.objectContaining({
|
||||
webPlayerSharedControls: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused store test and verify the red state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test services --skip-nx-cache --runInBand \
|
||||
--testPathPatterns=settings-store.service.spec
|
||||
```
|
||||
|
||||
Expected: FAIL because `getSettings()` omits
|
||||
`webPlayerSharedControls`, and the preference is not persisted.
|
||||
|
||||
- [ ] **Step 3: Add the shared settings field**
|
||||
|
||||
Add this property beside `player` in
|
||||
`libs/shared/interfaces/src/lib/settings.interface.ts`:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Use IPTVnator's shared controls in HTML5, Video.js, and ArtPlayer.
|
||||
* Missing values remain off for compatibility with older saved settings.
|
||||
*/
|
||||
webPlayerSharedControls?: boolean;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Normalize and serialize the setting**
|
||||
|
||||
In `libs/services/src/lib/settings-store.service.ts`, add the default:
|
||||
|
||||
```ts
|
||||
webPlayerSharedControls: false,
|
||||
```
|
||||
|
||||
Insert it immediately after `player: VideoPlayer.VideoJs` in
|
||||
`DEFAULT_SETTINGS`. After `...storedSettings` in `loadSettings()`, override
|
||||
untrusted stored input with an exact boolean:
|
||||
|
||||
```ts
|
||||
patchState(store, {
|
||||
...DEFAULT_SETTINGS,
|
||||
...storedSettings,
|
||||
webPlayerSharedControls: storedSettings.webPlayerSharedControls === true,
|
||||
dashboardRails: normalizeDashboardRailsSettings(
|
||||
storedSettings.dashboardRails
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
Include the resolved field in `getSettings()`:
|
||||
|
||||
```ts
|
||||
webPlayerSharedControls:
|
||||
store.webPlayerSharedControls?.() === true,
|
||||
```
|
||||
|
||||
Insert it immediately after `player: store.player()`.
|
||||
|
||||
Once the interface exists, simplify the four new tests to use direct typed
|
||||
access and typed updates:
|
||||
|
||||
```ts
|
||||
expect(store.getSettings().webPlayerSharedControls).toBe(false);
|
||||
expect(store.getSettings().webPlayerSharedControls).toBe(true);
|
||||
await store.updateSettings({ webPlayerSharedControls: true });
|
||||
```
|
||||
|
||||
Keep the `unknown as Partial<Settings>` cast only in the malformed-value test.
|
||||
|
||||
- [ ] **Step 5: Run contract and store tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx build shared-interfaces
|
||||
pnpm nx test services --skip-nx-cache --runInBand \
|
||||
--testPathPatterns=settings-store.service.spec
|
||||
```
|
||||
|
||||
Expected: both commands PASS; missing/malformed values resolve to `false`, and
|
||||
`true` is restored and persisted.
|
||||
|
||||
- [ ] **Step 6: Commit the settings contract**
|
||||
|
||||
```bash
|
||||
git add libs/shared/interfaces/src/lib/settings.interface.ts \
|
||||
libs/services/src/lib/settings-store.service.ts \
|
||||
libs/services/src/lib/settings-store.service.spec.ts
|
||||
git commit -m "feat(settings): persist shared web controls preference"
|
||||
```
|
||||
|
||||
## Task 2: Add the Conditional Playback Checkbox
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web/src/app/settings/settings-form.utils.ts`
|
||||
- Modify: `apps/web/src/app/settings/settings-playback-section.component.ts`
|
||||
- Modify: `apps/web/src/app/settings/settings-playback-section.component.html`
|
||||
- Test: `apps/web/src/app/settings/settings-playback-section.component.spec.ts`
|
||||
- Test: `apps/web/src/app/settings/settings.component.spec.ts`
|
||||
- Modify: `apps/web/src/assets/i18n/ar.json`
|
||||
- Modify: `apps/web/src/assets/i18n/ary.json`
|
||||
- Modify: `apps/web/src/assets/i18n/by.json`
|
||||
- Modify: `apps/web/src/assets/i18n/de.json`
|
||||
- Modify: `apps/web/src/assets/i18n/el.json`
|
||||
- Modify: `apps/web/src/assets/i18n/en.json`
|
||||
- Modify: `apps/web/src/assets/i18n/es.json`
|
||||
- Modify: `apps/web/src/assets/i18n/fr.json`
|
||||
- Modify: `apps/web/src/assets/i18n/it.json`
|
||||
- Modify: `apps/web/src/assets/i18n/ja.json`
|
||||
- Modify: `apps/web/src/assets/i18n/ko.json`
|
||||
- Modify: `apps/web/src/assets/i18n/nl.json`
|
||||
- Modify: `apps/web/src/assets/i18n/pl.json`
|
||||
- Modify: `apps/web/src/assets/i18n/pt.json`
|
||||
- Modify: `apps/web/src/assets/i18n/ru.json`
|
||||
- Modify: `apps/web/src/assets/i18n/tr.json`
|
||||
- Modify: `apps/web/src/assets/i18n/zh.json`
|
||||
- Modify: `apps/web/src/assets/i18n/zhtw.json`
|
||||
|
||||
- [ ] **Step 1: Write failing visibility and binding tests**
|
||||
|
||||
Add `webPlayerSharedControls` to the local `createForm()` helper in
|
||||
`settings-playback-section.component.spec.ts`:
|
||||
|
||||
```ts
|
||||
webPlayerSharedControls: new FormControl(false),
|
||||
```
|
||||
|
||||
Add the following tests:
|
||||
|
||||
```ts
|
||||
it.each([VideoPlayer.VideoJs, VideoPlayer.Html5Player, VideoPlayer.ArtPlayer])(
|
||||
'shows shared controls for the %s web player',
|
||||
(player) => {
|
||||
fixture.componentRef.setInput('form', createForm(player));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'[data-test-id="web-player-shared-controls-setting"]'
|
||||
)
|
||||
).not.toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
it.each([VideoPlayer.EmbeddedMpv, VideoPlayer.MPV, VideoPlayer.VLC])(
|
||||
'hides shared web controls for the %s player',
|
||||
(player) => {
|
||||
fixture.componentRef.setInput('form', createForm(player));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'[data-test-id="web-player-shared-controls-setting"]'
|
||||
)
|
||||
).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
it('binds the shared controls checkbox to the settings form', () => {
|
||||
const form = createForm(VideoPlayer.VideoJs);
|
||||
form.controls['webPlayerSharedControls'].setValue(true);
|
||||
fixture.componentRef.setInput('form', form);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector<HTMLInputElement>(
|
||||
'[data-test-id="web-player-shared-controls-toggle"] input[type="checkbox"]'
|
||||
)?.checked
|
||||
).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
In `settings.component.spec.ts`, add this field to `DEFAULT_SETTINGS`:
|
||||
|
||||
```ts
|
||||
webPlayerSharedControls: false,
|
||||
```
|
||||
|
||||
Add explicit hydration and save assertions:
|
||||
|
||||
```ts
|
||||
it('hydrates the shared web controls preference', () => {
|
||||
const mockStore = settingsStore as unknown as MockSettingsStore;
|
||||
mockStore._setSettings({
|
||||
webPlayerSharedControls: true,
|
||||
});
|
||||
|
||||
component.setSettings();
|
||||
|
||||
expect(component.settingsForm.get('webPlayerSharedControls')?.value).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('saves the shared web controls preference', async () => {
|
||||
const mockStore = settingsStore as unknown as MockSettingsStore;
|
||||
mockStore.updateSettings.mockResolvedValue(undefined);
|
||||
|
||||
component.settingsForm.get('webPlayerSharedControls')?.setValue(true);
|
||||
component.onSubmit();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(mockStore.updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
webPlayerSharedControls: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run focused web tests and verify the red state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test web --skip-nx-cache --runInBand \
|
||||
--testPathPatterns='settings-playback-section.component.spec|settings.component.spec'
|
||||
```
|
||||
|
||||
Expected: FAIL because the production form and Playback section do not yet
|
||||
declare or render the preference.
|
||||
|
||||
- [ ] **Step 3: Add form hydration and serialization**
|
||||
|
||||
In `createSettingsForm()` in
|
||||
`apps/web/src/app/settings/settings-form.utils.ts`, add:
|
||||
|
||||
```ts
|
||||
webPlayerSharedControls: false,
|
||||
```
|
||||
|
||||
Insert it immediately after the existing `player` control.
|
||||
|
||||
In `createSettingsFromFormValue()`, add:
|
||||
|
||||
```ts
|
||||
webPlayerSharedControls: value.webPlayerSharedControls ?? false,
|
||||
```
|
||||
|
||||
Insert it immediately after the existing `player` mapping.
|
||||
|
||||
The existing `SettingsComponent.setSettings()` `patchValue()` call then
|
||||
hydrates the checkbox without a new component-specific code path.
|
||||
|
||||
- [ ] **Step 4: Add the selected-engine predicate and checkbox**
|
||||
|
||||
Add this method to
|
||||
`apps/web/src/app/settings/settings-playback-section.component.ts`:
|
||||
|
||||
```ts
|
||||
isWebPlayerSelected(): boolean {
|
||||
const player = this.form().value.player;
|
||||
return (
|
||||
player === VideoPlayer.VideoJs ||
|
||||
player === VideoPlayer.Html5Player ||
|
||||
player === VideoPlayer.ArtPlayer
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Insert this row in
|
||||
`apps/web/src/app/settings/settings-playback-section.component.html` after the
|
||||
stream-format selector:
|
||||
|
||||
```html
|
||||
@if (isWebPlayerSelected()) {
|
||||
<div class="setting-item" data-test-id="web-player-shared-controls-setting">
|
||||
<div class="setting-item__meta">
|
||||
<h4>{{ 'SETTINGS.WEB_PLAYER_SHARED_CONTROLS' | translate }}</h4>
|
||||
<p>
|
||||
{{ 'SETTINGS.WEB_PLAYER_SHARED_CONTROLS_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="setting-item__control setting-item__toggle">
|
||||
<mat-checkbox
|
||||
formControlName="webPlayerSharedControls"
|
||||
data-test-id="web-player-shared-controls-toggle"
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run focused web tests and verify the green state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test web --skip-nx-cache --runInBand \
|
||||
--testPathPatterns='settings-playback-section.component.spec|settings.component.spec'
|
||||
```
|
||||
|
||||
Expected: PASS for all three visible web players, all three hidden non-web
|
||||
players, form hydration, and Save serialization.
|
||||
|
||||
- [ ] **Step 6: Add exact locale values**
|
||||
|
||||
Add `WEB_PLAYER_SHARED_CONTROLS` and
|
||||
`WEB_PLAYER_SHARED_CONTROLS_DESCRIPTION` beside
|
||||
`VIDEO_PLAYER_DESCRIPTION` inside `SETTINGS` in all 18 locale files:
|
||||
|
||||
| Locale | `WEB_PLAYER_SHARED_CONTROLS` | `WEB_PLAYER_SHARED_CONTROLS_DESCRIPTION` |
|
||||
| ------ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `en` | Unified controls for web players (experimental) | Use IPTVnator's shared controls in HTML5, Video.js, and ArtPlayer. |
|
||||
| `ru` | Единые элементы управления для веб-плееров (экспериментально) | Использовать общие элементы управления IPTVnator в HTML5, Video.js и ArtPlayer. |
|
||||
| `de` | Einheitliche Steuerung für Web-Player (experimentell) | Verwendet die gemeinsamen IPTVnator-Steuerelemente in HTML5, Video.js und ArtPlayer. |
|
||||
| `es` | Controles unificados para reproductores web (experimental) | Usa los controles compartidos de IPTVnator en HTML5, Video.js y ArtPlayer. |
|
||||
| `fr` | Commandes unifiées pour les lecteurs web (expérimental) | Utilise les commandes partagées d’IPTVnator dans HTML5, Video.js et ArtPlayer. |
|
||||
| `it` | Controlli unificati per i player web (sperimentale) | Usa i controlli condivisi di IPTVnator in HTML5, Video.js e ArtPlayer. |
|
||||
| `pt` | Controlos unificados para leitores web (experimental) | Usa os controlos partilhados do IPTVnator em HTML5, Video.js e ArtPlayer. |
|
||||
| `nl` | Uniforme bediening voor webspelers (experimenteel) | Gebruik de gedeelde IPTVnator-bediening in HTML5, Video.js en ArtPlayer. |
|
||||
| `pl` | Ujednolicone sterowanie odtwarzaczami internetowymi (eksperymentalne) | Używaj wspólnych elementów sterowania IPTVnator w HTML5, Video.js i ArtPlayer. |
|
||||
| `tr` | Web oynatıcıları için birleşik kontroller (deneysel) | HTML5, Video.js ve ArtPlayer'da IPTVnator'ın ortak kontrollerini kullan. |
|
||||
| `el` | Ενοποιημένα στοιχεία ελέγχου για web players (πειραματικό) | Χρησιμοποιήστε τα κοινά στοιχεία ελέγχου του IPTVnator σε HTML5, Video.js και ArtPlayer. |
|
||||
| `ja` | Webプレーヤーの統一コントロール(試験的) | HTML5、Video.js、ArtPlayerでIPTVnator共通のコントロールを使用します。 |
|
||||
| `ko` | 웹 플레이어 통합 컨트롤(실험적) | HTML5, Video.js 및 ArtPlayer에서 IPTVnator 공통 컨트롤을 사용합니다. |
|
||||
| `zh` | Web 播放器统一控件(实验性) | 在 HTML5、Video.js 和 ArtPlayer 中使用 IPTVnator 的共享控件。 |
|
||||
| `zhtw` | Web 播放器統一控制項(實驗性) | 在 HTML5、Video.js 和 ArtPlayer 中使用 IPTVnator 的共用控制項。 |
|
||||
| `ar` | عناصر تحكم موحّدة لمشغلات الويب (تجريبي) | استخدم عناصر التحكم المشتركة في IPTVnator مع HTML5 وVideo.js وArtPlayer. |
|
||||
| `ary` | عناصر تحكم موحدة لمشغلات الويب (تجريبية) | استعمل عناصر التحكم المشتركة ديال IPTVnator مع HTML5 وVideo.js وArtPlayer. |
|
||||
| `by` | Адзіныя элементы кіравання для вэб-прайгравальнікаў (эксперыментальна) | Выкарыстоўваць агульныя элементы кіравання IPTVnator у HTML5, Video.js і ArtPlayer. |
|
||||
|
||||
- [ ] **Step 7: Validate locale parity and the full Settings project**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run i18n:check
|
||||
pnpm nx test web --skip-nx-cache
|
||||
pnpm nx lint web --skip-nx-cache
|
||||
```
|
||||
|
||||
Expected: all commands PASS, with no missing or extra locale keys.
|
||||
|
||||
- [ ] **Step 8: Commit the Settings UI**
|
||||
|
||||
```bash
|
||||
git add apps/web/src/app/settings/settings-form.utils.ts \
|
||||
apps/web/src/app/settings/settings-playback-section.component.ts \
|
||||
apps/web/src/app/settings/settings-playback-section.component.html \
|
||||
apps/web/src/app/settings/settings-playback-section.component.spec.ts \
|
||||
apps/web/src/app/settings/settings.component.spec.ts \
|
||||
apps/web/src/assets/i18n
|
||||
git commit -m "feat(settings): expose shared web controls toggle"
|
||||
```
|
||||
|
||||
## Task 3: Resolve an Immutable Controls Mode per Player Host
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts`
|
||||
- Test:
|
||||
`libs/ui/playback/src/lib/web-player-view/web-player-view.component.shared-controls.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Write a failing host-provider test**
|
||||
|
||||
Update the Angular import in
|
||||
`web-player-view.component.shared-controls.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { Component, input, output, signal } from '@angular/core';
|
||||
```
|
||||
|
||||
Import the store and rollout token:
|
||||
|
||||
```ts
|
||||
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
|
||||
import { WEB_PLAYER_SHARED_CONTROLS } from '../player-controls';
|
||||
```
|
||||
|
||||
Add a mutable mock signal beside the suite variables:
|
||||
|
||||
```ts
|
||||
const webPlayerSharedControls = signal(false);
|
||||
```
|
||||
|
||||
Reset and provide it in `beforeEach()`:
|
||||
|
||||
```ts
|
||||
webPlayerSharedControls.set(false);
|
||||
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: { webPlayerSharedControls },
|
||||
},
|
||||
```
|
||||
|
||||
Insert the provider object into the existing TestBed `providers` array.
|
||||
|
||||
Add this test:
|
||||
|
||||
```ts
|
||||
it('snapshots the saved shared-controls mode for each player host', () => {
|
||||
webPlayerSharedControls.set(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.injector.get(WEB_PLAYER_SHARED_CONTROLS)).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
webPlayerSharedControls.set(false);
|
||||
expect(fixture.debugElement.injector.get(WEB_PLAYER_SHARED_CONTROLS)).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
fixture.destroy();
|
||||
fixture = TestBed.createComponent(WebPlayerViewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput(
|
||||
'streamUrl',
|
||||
'https://example.com/new-session.ts'
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.injector.get(WEB_PLAYER_SHARED_CONTROLS)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
This proves both required properties: Save affects the next host, and an
|
||||
existing host does not mix construction modes.
|
||||
|
||||
- [ ] **Step 2: Run the host test and verify the red state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test ui-playback --skip-nx-cache --runInBand \
|
||||
--testPathPatterns=web-player-view.component.shared-controls
|
||||
```
|
||||
|
||||
Expected: FAIL because the root token still resolves the compile-time `false`
|
||||
constant instead of the `SettingsStore` signal.
|
||||
|
||||
- [ ] **Step 3: Add the component-scoped token provider**
|
||||
|
||||
Update the services import in
|
||||
`libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts`:
|
||||
|
||||
```ts
|
||||
import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
|
||||
```
|
||||
|
||||
Import the rollout symbols:
|
||||
|
||||
```ts
|
||||
import {
|
||||
WEB_PLAYER_SHARED_CONTROLS,
|
||||
WEB_PLAYER_SHARED_CONTROLS_ENABLED,
|
||||
} from '../player-controls';
|
||||
```
|
||||
|
||||
Add a focused factory before the component decorator:
|
||||
|
||||
```ts
|
||||
function resolveWebPlayerSharedControls(): boolean {
|
||||
const storedValue = inject(SettingsStore).webPlayerSharedControls?.();
|
||||
|
||||
return typeof storedValue === 'boolean'
|
||||
? storedValue
|
||||
: WEB_PLAYER_SHARED_CONTROLS_ENABLED;
|
||||
}
|
||||
```
|
||||
|
||||
Add the component-scoped provider:
|
||||
|
||||
```ts
|
||||
providers: [
|
||||
{
|
||||
provide: WEB_PLAYER_SHARED_CONTROLS,
|
||||
useFactory: resolveWebPlayerSharedControls,
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
Insert this property into the existing `@Component` metadata.
|
||||
|
||||
Angular creates and caches this primitive in the `WebPlayerViewComponent`
|
||||
element injector. HTML5, Video.js, and ArtPlayer descendants receive one
|
||||
immutable value for their shared construction-time branch. Embedded MPV does
|
||||
not inject this token and therefore remains engine-driven.
|
||||
|
||||
- [ ] **Step 4: Run focused and complete playback tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test ui-playback --skip-nx-cache --runInBand \
|
||||
--testPathPatterns='web-player-view.component|web-player-controls.flag'
|
||||
pnpm nx test ui-playback --skip-nx-cache
|
||||
pnpm nx lint ui-playback --skip-nx-cache
|
||||
```
|
||||
|
||||
Expected: all commands PASS. Existing engine suites continue to prove that
|
||||
flag-off preserves vendor controls and flag-on mounts exactly one shared UI.
|
||||
|
||||
- [ ] **Step 5: Commit the playback wiring**
|
||||
|
||||
```bash
|
||||
git add libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts \
|
||||
libs/ui/playback/src/lib/web-player-view/web-player-view.component.shared-controls.spec.ts
|
||||
git commit -m "feat(playback): resolve shared controls from settings"
|
||||
```
|
||||
|
||||
## Task 4: Prove Persistence and Next-Session Activation End to End
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web-e2e/src/settings.e2e.ts`
|
||||
- Modify: `apps/electron-backend-e2e/src/settings.e2e.ts`
|
||||
|
||||
- [ ] **Step 1: Add browser Save/reload persistence coverage**
|
||||
|
||||
Add this test to `apps/web-e2e/src/settings.e2e.ts`:
|
||||
|
||||
```ts
|
||||
test('@settings @web Enable shared web player controls', async ({ page }) => {
|
||||
await openSettings(page);
|
||||
|
||||
const setting = page.locator(
|
||||
'[data-test-id="web-player-shared-controls-setting"]'
|
||||
);
|
||||
const checkbox = setting.locator('input[type="checkbox"]');
|
||||
|
||||
await expect(setting).toBeVisible();
|
||||
await expect(checkbox).not.toBeChecked();
|
||||
await checkbox.check();
|
||||
await saveSettings(page);
|
||||
|
||||
await page.reload();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(checkbox).toBeChecked();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extend the Electron restart-persistence test**
|
||||
|
||||
In the existing
|
||||
`persists changed desktop settings across app restart` test, immediately after
|
||||
selecting `html5`, enable the new checkbox:
|
||||
|
||||
```ts
|
||||
const sharedControlsCheckbox = firstLaunch.mainWindow
|
||||
.getByTestId('web-player-shared-controls-setting')
|
||||
.locator('input[type="checkbox"]');
|
||||
await expect(sharedControlsCheckbox).toBeVisible();
|
||||
await sharedControlsCheckbox.check();
|
||||
```
|
||||
|
||||
After the second launch confirms that HTML5 is selected, assert:
|
||||
|
||||
```ts
|
||||
await expect(
|
||||
secondLaunch.mainWindow
|
||||
.getByTestId('web-player-shared-controls-setting')
|
||||
.locator('input[type="checkbox"]')
|
||||
).toBeChecked();
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add a desktop next-session playback smoke test**
|
||||
|
||||
Add this test to `apps/electron-backend-e2e/src/settings.e2e.ts`:
|
||||
|
||||
```ts
|
||||
test('@settings @playback @electron applies shared controls to the next HTML5 session', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await openSettings(app.mainWindow);
|
||||
await selectSettingsOption(
|
||||
app.mainWindow,
|
||||
'select-video-player',
|
||||
'html5'
|
||||
);
|
||||
await app.mainWindow
|
||||
.getByTestId('web-player-shared-controls-setting')
|
||||
.locator('input[type="checkbox"]')
|
||||
.check();
|
||||
await saveSettings(app.mainWindow);
|
||||
|
||||
await goToDashboard(app.mainWindow);
|
||||
await importM3uPlaylistFromNativeDialog(app, m3uFixturePath);
|
||||
await app.mainWindow.waitForURL(/\/workspace\/playlists\/.+/);
|
||||
|
||||
const firstChannel = channelItemByTitle(
|
||||
app.mainWindow,
|
||||
'Channel 1'
|
||||
).first();
|
||||
await expect(firstChannel).toBeVisible({ timeout: 20000 });
|
||||
await firstChannel.click();
|
||||
|
||||
await expect(
|
||||
app.mainWindow.locator('app-html-video-player app-player-controls')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
app.mainWindow.locator('app-html-video-player video[controls]')
|
||||
).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run atomized web and Electron settings E2E**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx run web-e2e:e2e-ci--src/settings.e2e.ts
|
||||
pnpm nx run electron-backend-e2e:e2e-ci--src/settings.e2e.ts
|
||||
```
|
||||
|
||||
Expected: both targets PASS. Browser settings survive reload, desktop settings
|
||||
survive a full app restart, and the first HTML5 session after Save renders
|
||||
`app-player-controls` without native video controls.
|
||||
|
||||
- [ ] **Step 5: Commit E2E coverage**
|
||||
|
||||
```bash
|
||||
git add apps/web-e2e/src/settings.e2e.ts \
|
||||
apps/electron-backend-e2e/src/settings.e2e.ts
|
||||
git commit -m "test(playback): cover shared controls setting"
|
||||
```
|
||||
|
||||
## Task 5: Update Canonical Documentation and Run the Validation Ladder
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `README.md`
|
||||
- Modify: `docs/architecture/player-controls-contract.md`
|
||||
- Modify: `AGENTS.md`
|
||||
- Modify: `CLAUDE.md`
|
||||
|
||||
- [ ] **Step 1: Update the top-level Playback feature list**
|
||||
|
||||
Add this bullet under `README.md` → `Features` → `Playback`:
|
||||
|
||||
```md
|
||||
- Optional unified IPTVnator controls for HTML5, Video.js, and ArtPlayer, enabled in **Settings → Playback** _(experimental)_
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the canonical controls contract**
|
||||
|
||||
In `docs/architecture/player-controls-contract.md`:
|
||||
|
||||
1. Replace “a default-off web rollout token” in the current-status list with:
|
||||
|
||||
```md
|
||||
- a persisted, default-off web-player preference resolved through an immutable
|
||||
per-host rollout token;
|
||||
```
|
||||
|
||||
2. Replace the compile-time-only current-status paragraph with:
|
||||
|
||||
```md
|
||||
`Settings.webPlayerSharedControls` remains default-off. `WebPlayerViewComponent`
|
||||
snapshots it into `WEB_PLAYER_SHARED_CONTROLS` when a new player host is
|
||||
created, so HTML5, Video.js, and ArtPlayer switch atomically without an
|
||||
application restart. Existing sessions never change controls mode in place.
|
||||
```
|
||||
|
||||
3. Replace the rollout-symbol table rows with:
|
||||
|
||||
```md
|
||||
| Symbol / setting | Default | Current effect |
|
||||
| ------------------------------------ | ---------------: | ----------------------------------------------------------------------- |
|
||||
| `Settings.webPlayerSharedControls` | `false` | Persisted experimental opt-in shown for HTML5, Video.js, and ArtPlayer. |
|
||||
| `WEB_PLAYER_SHARED_CONTROLS_ENABLED` | `false` | Default-off fallback for direct component use and focused tests. |
|
||||
| `WEB_PLAYER_SHARED_CONTROLS` | session snapshot | Component-scoped immutable value consumed by the three web engines. |
|
||||
```
|
||||
|
||||
4. State next to the Embedded MPV section:
|
||||
|
||||
```md
|
||||
The web-player preference does not affect Embedded MPV. Frame-copy always uses
|
||||
the shared DOM controls, while native-view keeps its compositor-safe legacy
|
||||
dock.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Keep agent documentation synchronized**
|
||||
|
||||
In both `AGENTS.md` and `CLAUDE.md`, update the shared-player-controls
|
||||
description to include these exact facts:
|
||||
|
||||
```md
|
||||
- The persisted `Settings.webPlayerSharedControls` preference is default-off
|
||||
and appears only when HTML5, Video.js, or ArtPlayer is selected.
|
||||
- `WebPlayerViewComponent` snapshots the preference into the component-scoped
|
||||
`WEB_PLAYER_SHARED_CONTROLS` token for each new host; an existing session
|
||||
never changes controls mode in place.
|
||||
- Embedded MPV ignores this preference: frame-copy always uses shared controls,
|
||||
native-view retains the legacy dock, and external MPV/VLC own their UI.
|
||||
```
|
||||
|
||||
Preserve the existing engine lifecycle details around these sentences.
|
||||
|
||||
- [ ] **Step 4: Format and inspect documentation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm exec prettier --write README.md \
|
||||
docs/architecture/player-controls-contract.md AGENTS.md CLAUDE.md
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: Prettier completes successfully and `git diff --check` prints no
|
||||
errors.
|
||||
|
||||
- [ ] **Step 5: Run the complete regression ladder**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx build shared-interfaces --skip-nx-cache
|
||||
pnpm nx test services --skip-nx-cache
|
||||
pnpm nx test web --skip-nx-cache
|
||||
pnpm nx test ui-playback --skip-nx-cache
|
||||
pnpm nx run-many --target=lint \
|
||||
--projects=shared-interfaces,services,web,ui-playback,web-e2e,electron-backend-e2e \
|
||||
--skip-nx-cache
|
||||
pnpm run typecheck:web
|
||||
pnpm run i18n:check
|
||||
pnpm nx build web --configuration=production --skip-nx-cache
|
||||
pnpm nx run web-e2e:e2e-ci--src/settings.e2e.ts
|
||||
pnpm nx run electron-backend-e2e:e2e-ci--src/settings.e2e.ts
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: every command exits `0`. Record command results in the final handoff;
|
||||
if a platform-dependent Electron E2E cannot run, report the exact environment
|
||||
constraint and the strongest completed substitute instead of claiming it
|
||||
passed.
|
||||
|
||||
- [ ] **Step 6: Review the final diff against the approved design**
|
||||
|
||||
Confirm all of the following before committing:
|
||||
|
||||
- Default and malformed stored values resolve to `false`.
|
||||
- Only HTML5, Video.js, and ArtPlayer expose the checkbox.
|
||||
- Save affects the next player host without an app restart.
|
||||
- One host receives one immutable boolean mode.
|
||||
- Frame-copy Embedded MPV still always uses shared controls.
|
||||
- Native-view Embedded MPV and external MPV/VLC remain unchanged.
|
||||
- All 18 locales contain both keys.
|
||||
- Unit, lint, typecheck, build, web E2E, and Electron E2E results are recorded.
|
||||
- `README.md`, `docs/architecture/player-controls-contract.md`, `AGENTS.md`,
|
||||
and `CLAUDE.md` describe the shipped behavior consistently.
|
||||
|
||||
- [ ] **Step 7: Commit documentation and final integration**
|
||||
|
||||
```bash
|
||||
git add README.md docs/architecture/player-controls-contract.md \
|
||||
AGENTS.md CLAUDE.md
|
||||
git commit -m "docs(playback): document shared controls preference"
|
||||
```
|
||||
@@ -0,0 +1,210 @@
|
||||
# Web-Player Shared Controls Setting Design
|
||||
|
||||
## Context
|
||||
|
||||
The shared `app-player-controls` presentation is already integrated with four
|
||||
playback paths:
|
||||
|
||||
- Embedded MPV frame-copy uses it unconditionally because its video is rendered
|
||||
into a DOM canvas.
|
||||
- HTML5, Video.js, and ArtPlayer can use it behind the default-off
|
||||
`WEB_PLAYER_SHARED_CONTROLS` rollout token.
|
||||
|
||||
The web-player token currently resolves from the compile-time
|
||||
`WEB_PLAYER_SHARED_CONTROLS_ENABLED = false` constant. Enabling the integrations
|
||||
therefore requires a code change, rebuild, and application restart. The
|
||||
integrations are ready for opt-in testing, so the rollout choice should be a
|
||||
persisted user setting instead.
|
||||
|
||||
The Settings route does not keep an active player mounted. The setting can
|
||||
therefore be applied when the next playback session is created; changing an
|
||||
already-running engine or preserving an active playback position is not
|
||||
required.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add an experimental Playback checkbox for opting HTML5, Video.js, and
|
||||
ArtPlayer into the shared controls.
|
||||
- Persist the choice in the canonical `SettingsStore`.
|
||||
- Apply the saved choice to the next web-player session without restarting or
|
||||
reloading the application.
|
||||
- Keep the controls mode immutable for the lifetime of a player session.
|
||||
- Preserve default-off behavior and compatibility with existing stored
|
||||
settings.
|
||||
- Keep Embedded MPV and external-player behavior unchanged.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Make shared web controls the default.
|
||||
- Change an active player between legacy and shared controls.
|
||||
- Add separate preferences for HTML5, Video.js, and ArtPlayer.
|
||||
- Make Embedded MPV native-view use a DOM overlay.
|
||||
- Allow frame-copy Embedded MPV to fall back to the legacy controls dock.
|
||||
- Control the native UI of external MPV or VLC processes.
|
||||
|
||||
## Embedded MPV Semantics
|
||||
|
||||
The new checkbox is intentionally web-player-specific.
|
||||
|
||||
Embedded MPV has two rendering paths:
|
||||
|
||||
- **Frame-copy:** video is uploaded to a renderer canvas. It always uses
|
||||
`app-player-controls`; a second controls toggle would only duplicate an
|
||||
already-required part of this engine.
|
||||
- **Native-view:** video is hosted in a platform-native surface. DOM controls
|
||||
cannot reliably stack above that surface, so the compositor-safe legacy dock
|
||||
remains required.
|
||||
|
||||
The existing `embeddedMpvFrameCopy` setting continues to select the rendering
|
||||
engine and continues to require an application restart. It is independent from
|
||||
the new web-player controls preference.
|
||||
|
||||
External MPV and VLC processes continue to own their own controls UI.
|
||||
|
||||
## Considered Approaches
|
||||
|
||||
### 1. Conditional global web-player preference
|
||||
|
||||
Selected.
|
||||
|
||||
One setting controls HTML5, Video.js, and ArtPlayer together. Its checkbox is
|
||||
shown only while one of those players is selected. This matches the existing
|
||||
single rollout token, keeps the Settings UI relevant to the selected player,
|
||||
and avoids suggesting that the option changes Embedded MPV or external
|
||||
players.
|
||||
|
||||
### 2. Always-visible "shared controls where available" preference
|
||||
|
||||
Rejected because it would be a no-op for frame-copy Embedded MPV, unsupported
|
||||
for native-view, and outside the app for external players. The resulting
|
||||
conditional semantics would be harder to explain than a web-player-specific
|
||||
label.
|
||||
|
||||
### 3. Separate preference for every playback engine
|
||||
|
||||
Rejected because the three web integrations deliberately share one rollout
|
||||
contract. Per-engine preferences would add settings and test combinations
|
||||
without a current product requirement.
|
||||
|
||||
## Settings Contract
|
||||
|
||||
Add an optional boolean to the shared settings interface:
|
||||
|
||||
```ts
|
||||
webPlayerSharedControls?: boolean;
|
||||
```
|
||||
|
||||
The canonical default is `false`. Loading an older settings object without the
|
||||
field must normalize to `false`, and saving settings must write the complete
|
||||
resolved value back to storage.
|
||||
|
||||
The settings form adds a `webPlayerSharedControls` control with a `false`
|
||||
default. Form hydration, serialization, reset behavior, backup/export, and the
|
||||
existing Save workflow continue to use the normal complete `Settings` object.
|
||||
This is renderer-only state; Electron main-process behavior and preload IPC do
|
||||
not need a new command.
|
||||
|
||||
## Runtime Data Flow
|
||||
|
||||
The existing `WEB_PLAYER_SHARED_CONTROLS` injection token remains the
|
||||
immutable, test-overridable session seam used by the three engine components.
|
||||
The compile-time constant remains only as the default-off fallback for direct
|
||||
component use and focused tests.
|
||||
|
||||
Normal runtime resolution moves to `WebPlayerViewComponent`:
|
||||
|
||||
1. `SettingsStore` loads `webPlayerSharedControls`, defaulting to `false`.
|
||||
2. The Settings page updates and persists it through the existing Save action.
|
||||
3. Leaving Settings destroys that route; no player is active during the
|
||||
change.
|
||||
4. The next `WebPlayerViewComponent` resolves a component-scoped boolean
|
||||
snapshot from `SettingsStore`.
|
||||
5. HTML5, Video.js, or ArtPlayer receives that snapshot through
|
||||
`WEB_PLAYER_SHARED_CONTROLS` and constructs exactly one controls system.
|
||||
6. The snapshot does not change until that player host is destroyed and a new
|
||||
playback session is created.
|
||||
|
||||
This preserves the construction-time requirements of Video.js and ArtPlayer:
|
||||
vendor controls, hotkeys, gestures, plugins, source bridges, and the shared
|
||||
adapter are configured atomically. A reactive boolean must not flip only the
|
||||
template while leaving the engine configured for the other mode.
|
||||
|
||||
Embedded MPV does not consume the setting. Its component continues to choose
|
||||
shared controls for `engine === 'frame-copy'` and the legacy dock for the
|
||||
native-view engine.
|
||||
|
||||
## Settings UI
|
||||
|
||||
Add one standard `setting-item` to the Playback section, using the existing
|
||||
Material checkbox and layout styles.
|
||||
|
||||
- Label meaning: **Use unified controls for web players (experimental)**.
|
||||
- Description meaning: use IPTVnator's common controls in HTML5, Video.js, and
|
||||
ArtPlayer.
|
||||
- Visibility: selected player is `videojs`, `html5`, or `artplayer`.
|
||||
- Hidden for Embedded MPV, external MPV, and VLC.
|
||||
- Add a stable test selector to the row and checkbox.
|
||||
- Add the label and description keys to every locale file so the i18n key sets
|
||||
remain aligned.
|
||||
|
||||
No new colors, spacing rules, or one-off UI components are required.
|
||||
|
||||
## Failure and Compatibility Behavior
|
||||
|
||||
- Missing or malformed non-boolean stored values resolve to `false`.
|
||||
- A storage write failure is reported through the existing
|
||||
`SettingsStore.updateSettings()` rejection and must not require
|
||||
player-specific recovery.
|
||||
- Legacy mode remains the safe fallback whenever the setting cannot be
|
||||
resolved.
|
||||
- Direct engine tests can continue overriding
|
||||
`WEB_PLAYER_SHARED_CONTROLS` with `true` or `false`.
|
||||
- The setting does not relax Electron sandboxing and does not alter the
|
||||
frame-copy engine opt-in.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Shared interfaces and settings store
|
||||
|
||||
- Default is `false`.
|
||||
- Older stored settings without the field load as `false`.
|
||||
- A stored `true` value is restored.
|
||||
- `updateSettings()` persists the complete resolved value.
|
||||
|
||||
### Settings form and UI
|
||||
|
||||
- Form creation and serialization include the new field.
|
||||
- The checkbox hydrates from stored settings and is included in the Save
|
||||
payload.
|
||||
- The row is visible for HTML5, Video.js, and ArtPlayer.
|
||||
- The row is hidden for Embedded MPV, external MPV, and VLC.
|
||||
- Translation key parity remains valid across all locales.
|
||||
|
||||
### Playback host
|
||||
|
||||
- A newly created HTML5, Video.js, or ArtPlayer session receives the saved
|
||||
boolean snapshot.
|
||||
- `false` preserves the existing vendor/native UI.
|
||||
- `true` mounts the shared controls path.
|
||||
- Embedded MPV selection ignores the web-player preference.
|
||||
- One session never renders or initializes both controls systems.
|
||||
|
||||
### Validation
|
||||
|
||||
- Run focused Settings form/component and `SettingsStore` tests.
|
||||
- Run the complete `ui-playback` test target because all three web engines
|
||||
share the rollout seam.
|
||||
- Run affected lint targets and the workspace typecheck.
|
||||
- Run the settings E2E flow for checkbox visibility, persistence, and use after
|
||||
returning to playback.
|
||||
- Run the closest Electron playback smoke/E2E coverage because the same
|
||||
Settings surface is used in the desktop app.
|
||||
|
||||
## Documentation Impact
|
||||
|
||||
Update `docs/architecture/player-controls-contract.md` and the shared-controls
|
||||
sections of `AGENTS.md` and `CLAUDE.md` to replace the compile-time-only
|
||||
default-off rollout description with the persisted experimental preference.
|
||||
The Embedded MPV architecture documentation only needs clarification if its
|
||||
existing distinction between frame-copy shared controls and the native-view
|
||||
legacy dock would otherwise become ambiguous.
|
||||
Reference in new issue
Block a user