Files
iptvnator/docs/superpowers/plans/2026-07-17-web-player-shared-controls-setting.md
T
4gray beb62db314 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
2026-07-17 13:38:06 +02:00

31 KiB
Raw Blame History

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:

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:

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:

/**
 * 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:

webPlayerSharedControls: false,

Insert it immediately after player: VideoPlayer.VideoJs in DEFAULT_SETTINGS. After ...storedSettings in loadSettings(), override untrusted stored input with an exact boolean:

patchState(store, {
    ...DEFAULT_SETTINGS,
    ...storedSettings,
    webPlayerSharedControls: storedSettings.webPlayerSharedControls === true,
    dashboardRails: normalizeDashboardRailsSettings(
        storedSettings.dashboardRails
    ),
});

Include the resolved field in getSettings():

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:

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:

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
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:

webPlayerSharedControls: new FormControl(false),

Add the following tests:

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:

webPlayerSharedControls: false,

Add explicit hydration and save assertions:

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:

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:

webPlayerSharedControls: false,

Insert it immediately after the existing player control.

In createSettingsFromFormValue(), add:

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:

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:

@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:

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:

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
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:

import { Component, input, output, signal } from '@angular/core';

Import the store and rollout token:

import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';
import { WEB_PLAYER_SHARED_CONTROLS } from '../player-controls';

Add a mutable mock signal beside the suite variables:

const webPlayerSharedControls = signal(false);

Reset and provide it in beforeEach():

webPlayerSharedControls.set(false);

{
    provide: SettingsStore,
    useValue: { webPlayerSharedControls },
},

Insert the provider object into the existing TestBed providers array.

Add this test:

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:

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:

import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services';

Import the rollout symbols:

import {
    WEB_PLAYER_SHARED_CONTROLS,
    WEB_PLAYER_SHARED_CONTROLS_ENABLED,
} from '../player-controls';

Add a focused factory before the component decorator:

function resolveWebPlayerSharedControls(): boolean {
    const storedValue = inject(SettingsStore).webPlayerSharedControls?.();

    return typeof storedValue === 'boolean'
        ? storedValue
        : WEB_PLAYER_SHARED_CONTROLS_ENABLED;
}

Add the component-scoped provider:

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:

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
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:

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:

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:

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:

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:

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
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:

- 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:
- a persisted, default-off web-player preference resolved through an immutable
  per-host rollout token;
  1. Replace the compile-time-only current-status paragraph with:
`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.
  1. Replace the rollout-symbol table rows with:
| 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.     |
  1. State next to the Embedded MPV section:
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:

- 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:

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:

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

git add README.md docs/architecture/player-controls-contract.md \
  AGENTS.md CLAUDE.md
git commit -m "docs(playback): document shared controls preference"