diff --git a/AGENTS.md b/AGENTS.md index 5ce8b0b17..2af3566df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,8 +147,20 @@ Key files: commands are cancelled. Same-session IPC replies also yield to a broadcast snapshot received while the command was pending, preventing a successful recording acknowledgement from being rolled back by a stale reply. -- HTML5/hls.js, Video.js, and ArtPlayer are not wired yet. Their existing skins - remain active and the web rollout token remains default-off. +- The built-in HTML5/hls.js player is the second guarded consumer. + `HtmlVideoPlayerComponent` provides a component-scoped + `WebVideoControlsAdapter`; its player-local bridge owns HLS/native tracks, + MPEG-TS VOD duration correction, caption preference, and source cleanup. + `HtmlVideoElementSession` owns native video-event lifecycle, persisted + volume, start-time/time/ended propagation, and legacy post-play caption + suppression. + `WebPlayerViewComponent.resolvedIsLive` supplies authoritative live/VOD + metadata, while a visible playback diagnostic disables both shared surface + interaction and shortcuts and exits the HTML5 shell's own fullscreen so the + diagnostic actions remain visible. The flag-off path keeps native controls + and legacy series navigation unchanged. +- Video.js and ArtPlayer are not wired yet. Their existing skins remain active, + and the web rollout token remains default-off. - Canonical docs: `docs/architecture/player-controls-contract.md` and `docs/architecture/embedded-mpv-native.md` diff --git a/CLAUDE.md b/CLAUDE.md index 4bcb10a41..20947f568 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -618,7 +618,7 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use - External players: MPV, VLC (via IPC to Electron backend) - Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a native addon. macOS uses the libmpv render API in an `NSOpenGLView`; Windows uses in-process libmpv with `--wid` against an app-owned child `HWND`; Linux spawns an out-of-process `mpv --wid=` controlled over a JSON IPC socket (X11/XWayland only, requires system `mpv` on PATH; subtitles/speed/aspect/recording are not exported there). mpv's own screensaver inhibition does not apply to any of these paths, so `EmbeddedMpvNativeService` holds an Electron `powerSaveBlocker` (`prevent-display-sleep`) whenever any session's status is `playing`, and releases it on pause, dispose, or shutdown. Service: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`; full architecture: `docs/architecture/embedded-mpv-native.md`. - Embedded MPV frame-copy engine (experimental, macOS Apple Silicon + Linux + Windows; enabled via `Settings > Playback > Embedded MPV: frame-copy engine` (restart required) or `IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1` on top of the embedded MPV experiment flag): a per-session helper renders mpv offscreen at viewport size (headless CGL on macOS, headless EGL on Linux, WGL against a hidden window on Windows) and publishes BGRA frames into a shm ring (POSIX shm; a `Local\` named file mapping on Windows); the preload frame pump uploads them onto a renderer ``, so controls/dialogs are ordinary DOM above the video. Frame-copy is the first runtime consumer of shared `app-player-controls`: `PlayerControlsComponent` and its surface/shortcut/fullscreen collaborators own the DOM UI interactions, while the component-scoped `EmbeddedMpvControlsAdapter` maps session state and commands and coordinates correlated recording state; native-view retains the legacy fixed dock. Stored and explicit opt-ins relax the sandbox only while the base embedded-MPV feature is enabled and a platform-supported packaged runtime contains both the regular-file helper (`iptvnator_mpv_helper` / `.exe`) and readable regular frame-reader addon; packaged discovery is restricted to packaged resources. A disabled base experiment keeps embedded MPV unavailable with the sandbox intact, while a missing, mode-stripped, or incomplete frame-copy runtime falls back to the native engine without relaxing the sandbox. On Linux the engine is dev-build-only for now: the helper links system libmpv (build deps: `libmpv-dev`, `libegl-dev`, `libgl-dev`, `libopengl-dev`, `libgbm-dev`) and is stripped from packages until bundled-runtime staging lands. On Windows the helper links vendored libmpv and package validation requires the exact MPV DLL named in the helper's PE import table beside the executable. Backend process adapter: `apps/electron-backend/src/app/services/embedded-mpv-frame-copy.adapter.ts`; shared-controls adapter: `libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-controls.adapter.ts`; helper: `apps/electron-backend/native/helper/`; details in `docs/architecture/embedded-mpv-native.md` ("Frame-Copy Engine"). -- Shared player-controls layer: `libs/ui/playback/src/lib/player-controls/` exports the engine-neutral `PlayerController` contract, standalone `app-player-controls`, a generic web-video adapter/helper, and a default-off web rollout token. Embedded MPV frame-copy consumes it through `EmbeddedMpvControlsAdapter`; the host selects exactly one UI, so native-view retains its compositor-safe dock. `showControls=false` detaches the shared surface, modal overlays gate frame-copy playback shortcuts, fullscreen remains DOM-based with Embedded MPV bounds sync, and a playback/session transition key prevents engine or session handoff from presenting stale recording feedback while timers and pending commands are cancelled. Same-session IPC replies yield to a broadcast snapshot received while the command was pending, so a successful recording acknowledgement cannot be rolled back by a stale reply. HTML5/hls.js, Video.js, and ArtPlayer remain unwired with their existing skins. Contract: `docs/architecture/player-controls-contract.md`. +- Shared player-controls layer: `libs/ui/playback/src/lib/player-controls/` exports the engine-neutral `PlayerController` contract, standalone `app-player-controls`, a generic web-video adapter/helper, and a default-off web rollout token. Embedded MPV frame-copy consumes it through `EmbeddedMpvControlsAdapter`; the host selects exactly one UI, so native-view retains its compositor-safe dock. `showControls=false` detaches the shared surface, modal overlays gate frame-copy playback shortcuts, fullscreen remains DOM-based with Embedded MPV bounds sync, and a playback/session transition key prevents engine or session handoff from presenting stale recording feedback while timers and pending commands are cancelled. Same-session IPC replies yield to a broadcast snapshot received while the command was pending, so a successful recording acknowledgement cannot be rolled back by a stale reply. The built-in HTML5/hls.js player is the second guarded consumer: `HtmlVideoPlayerComponent` provides a component-scoped `WebVideoControlsAdapter`, while its local bridge owns HLS/native tracks, MPEG-TS VOD duration correction, caption preference, and source cleanup. `HtmlVideoElementSession` owns native video-event lifecycle, persisted volume, start-time/time/ended propagation, and legacy post-play caption suppression. `WebPlayerViewComponent.resolvedIsLive` supplies authoritative metadata; visible playback diagnostics disable shared pointer/keyboard ownership and exit the HTML5 shell's own fullscreen so retry/fallback actions remain visible. The default-off path retains native controls and legacy series navigation. Video.js and ArtPlayer remain unwired with their existing skins. Contract: `docs/architecture/player-controls-contract.md`. **VOD/Series Detail Pages (two-state layout)**: diff --git a/docs/architecture/player-controls-contract.md b/docs/architecture/player-controls-contract.md index 66a07b6ec..aa4aa3d97 100644 --- a/docs/architecture/player-controls-contract.md +++ b/docs/architecture/player-controls-contract.md @@ -7,8 +7,7 @@ Embedded MPV rendering and native-view bounds behavior remain documented in ## Current status -The shared-controls foundation from PR #1148 now has its first runtime -consumer: +The shared-controls foundation from PR #1148 now has two runtime consumers: - the `PlayerController` contract, default state, and capability presets; - the standalone `app-player-controls` presentation component and its @@ -17,7 +16,8 @@ consumer: - a default-off web rollout token; - the component-scoped `EmbeddedMpvControlsAdapter`; - an `EmbeddedMpvPlayerComponent` host integration for the frame-copy engine; - and +- a feature-flagged `HtmlVideoPlayerComponent` integration backed by + `WebVideoControlsAdapter` and a player-local engine bridge; and - focused unit/component tests. When Embedded MPV reports `engine: 'frame-copy'`, the component mounts @@ -26,10 +26,17 @@ When Embedded MPV reports `engine: 'frame-copy'`, the component mounts component keeps the existing compositor-safe controls dock. Exactly one of those control systems is active at a time. -Video.js, html5+hls.js, and ArtPlayer do not consume the shared layer yet. Their -existing skins remain active, and `WEB_PLAYER_SHARED_CONTROLS_ENABLED` remains -default-off with no runtime web host consuming it. Changing that token alone -does not switch any web player UI. +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 local 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. + +Video.js and ArtPlayer do not consume the shared layer yet. Their existing +skins remain active, and `WEB_PLAYER_SHARED_CONTROLS_ENABLED` remains +default-off, so the guarded HTML5 integration does not change normal runtime +behavior. This rollout is intentionally engine-selective: frame-copy can use normal DOM layering, while the native platform view cannot. The integration also includes @@ -75,19 +82,21 @@ contract does not make a native video surface behave like DOM content. ▼ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ WebVideoControlsAdapter │ │ EmbeddedMpvControlsAdapter │ -│ Landed, generic, not wired │ │ Landed, component-scoped │ -└──────────────────────────────┘ └──────────────┬───────────────┘ - │ - ▼ - ┌──────────────────────────────┐ - │ EmbeddedMpvPlayerComponent │ - │ frame-copy: shared controls │ - │ native-view: legacy dock │ - └──────────────────────────────┘ +│ Generic, component-scoped │ │ Component-scoped │ +└──────────────┬───────────────┘ └──────────────┬───────────────┘ + │ │ + ▼ ▼ +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ HtmlVideoPlayerComponent │ │ EmbeddedMpvPlayerComponent │ +│ flag on: shared controls │ │ frame-copy: shared controls │ +│ flag off: native controls │ │ native-view: legacy dock │ +└──────────────────────────────┘ └──────────────────────────────┘ ``` The embedded host selects controls from the reported engine before rendering them. It never mounts the shared overlay and legacy dock together. +The HTML5 host likewise selects native or shared controls before rendering and +never attaches the web adapter while the native path is active. ## The contract @@ -160,8 +169,8 @@ size follows the fullscreen DOM surface. ## Shared default controls `PlayerControlsComponent` is a standalone presentation component. The -frame-copy Embedded MPV host mounts it over its DOM canvas; future web hosts can -mount the same component over their playback surfaces. +frame-copy Embedded MPV host mounts it over its DOM canvas, and the guarded +HTML5 host mounts it over `.html-video-player-shell`. It owns only transient presentation behavior: @@ -218,6 +227,15 @@ a modal/backdrop overlay is active, so transport, seek, volume, and fullscreen actions cannot leak through it. Escape keeps the shared component's generic popover-dismissal behavior. +The HTML5 host applies the same ownership rule while its playback diagnostic is +visible: `WebPlayerViewComponent` passes +`interactionEnabled = visiblePlaybackDiagnostic() === null`, and the HTML5 +component binds that value to both `showControls` and `shortcutsEnabled`. +If that shell owns DOM fullscreen, the host exits fullscreen before hiding the +controls so the sibling diagnostic banner and its retry/fallback actions remain +visible; fullscreen owned by another element is left untouched. Retrying +playback or clearing the diagnostic restores both interaction paths. + Frame-copy recording transitions use the adapter's playback/session identity as their `transitionKey`. Session disposal, retry, channel changes, and engine handoff therefore clear stale recording ownership without showing a false @@ -245,15 +263,15 @@ capability, initialization runs again for the new capability epoch. The volume slider intentionally remains continuous: each volume `input` applies the optimistic volume immediately. -## Web adapter (landed, not wired) +## Web adapter and HTML5 engine bridge `WebVideoControlsAdapter` can translate an `HTMLVideoElement` into the shared contract. It uses DOM/media events and accepts optional engine-specific track accessors through `WebVideoControlsOptions`, so the adapter itself stays usable in the PWA and does not import a concrete web engine. -Native media events refresh the adapter automatically. A future engine host -must call the public `refresh()` hook after engine-specific getters change +Native media events refresh the adapter automatically. An engine host must call +the public `refresh()` hook after engine-specific getters change without a corresponding media event, including track lists, corrected duration, or live/VOD classification. Source, readiness, progress, seeking, and playback events that can invalidate the snapshot are observed directly. @@ -271,20 +289,34 @@ temporarily mislabeled as live. An attached element with no resource maps to `idle`, paused preload/warm-up remains playable, and only actively playing media with insufficient data maps to `loading`. -`web-video-controls.host.ts` contains small attachment/projection helpers for a -future host integration. No Video.js, html5+hls.js, or ArtPlayer component calls -those helpers. +`HtmlVideoPlayerControlsBridge` attaches the adapter to the HTML5 video element +and delegates engine-specific work to focused HLS and native-text-track +collaborators. HLS track IDs remain the list indices accepted by hls.js. Native +caption/subtitle IDs remain stable for the lifetime of a source through a +`WeakMap`, even when the browser removes or reorders tracks. Source replacement +removes track listeners before the old HLS instance is destroyed, resets any +per-source subtitle override, and leaves exactly one engine source bound. + +Live/VOD classification comes from `WebPlayerViewComponent.resolvedIsLive`: +explicit `ResolvedPortalPlayback.isLive` wins, otherwise content metadata means +VOD and its absence means live. The same computed value configures Video.js, +the HTML5 bridge, and mpegts.js; media duration is never used to infer the +classification. + +Raw MPEG-TS VOD can expose `video.duration === Infinity`. For that source only, +the bridge uses the first finite positive value from `video.duration`, the last +valid seekable end, or the last valid buffered end. Without a known duration it +keeps the source classified as VOD while seeking remains unavailable. + +`web-video-controls.host.ts` still contains small generic +attachment/projection helpers. Video.js and ArtPlayer do not call them yet. The rollout symbols are: -| Symbol | Default | Current effect | -| ------------------------------------ | ------------: | ----------------------------------------------------------------------------------- | -| `WEB_PLAYER_SHARED_CONTROLS_ENABLED` | `false` | Documents the intended web rollout default. | -| `WEB_PLAYER_SHARED_CONTROLS` | default above | Injectable/test-overridable view of the default. No runtime web player consumes it. | - -A follow-up web integration must explicitly consume the token, attach the -adapter to the active video element, mount `app-player-controls`, and only then -disable the engine's existing skin. +| 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 the HTML5 host to switch atomically between native and shared controls. | ## Embedded MPV rendering constraints @@ -335,9 +367,9 @@ renderer, bounds, and platform details. The remaining design seams are: -1. **Web hosts** — mount the component, consume the rollout token, attach - `WebVideoControlsAdapter`, and remove an engine skin only when the shared - controls are active. +1. **Video.js and ArtPlayer** — add engine-specific adapters/bridges, consume + the rollout token, and remove each engine skin only when shared controls are + active. 2. **Native-view UI** — retain the compositor-safe dock unless the native engine's compositing architecture changes independently. A native-view migration is not part of the frame-copy rollout. @@ -388,5 +420,24 @@ libs/ui/playback/src/lib/embedded-mpv-player/ ``` The adapter and recording helpers are component-scoped through -`EmbeddedMpvPlayerComponent`. Web host wiring, removal of web engine skins, and +`EmbeddedMpvPlayerComponent`. + +The guarded HTML5 integration lives in: + +```text +libs/ui/playback/src/lib/html-video-player/ +├── html-video-element-session.ts +├── html-video-player-controls.bridge.ts +├── html-video-player-hls-controls.ts +├── html-video-player-native-text-tracks.ts +├── html-video-player.component.ts +└── html-video-player.component.html +``` + +`HtmlVideoPlayerComponent` provides a component-scoped +`WebVideoControlsAdapter`. The bridge and its collaborators are player-local +because HLS/native track identity, caption preference, and cleanup are tied to +one active source. `HtmlVideoElementSession` separately owns native video-event +attachment, persisted volume, start-time/time/ended propagation, and the +flag-off post-play caption behavior. Video.js/ArtPlayer skin removal and persistent/background player ownership have not landed. diff --git a/docs/superpowers/plans/2026-07-16-html5-shared-controls.md b/docs/superpowers/plans/2026-07-16-html5-shared-controls.md new file mode 100644 index 000000000..dfe8c7564 --- /dev/null +++ b/docs/superpowers/plans/2026-07-16-html5-shared-controls.md @@ -0,0 +1,620 @@ +# HTML5 Shared Controls 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:** Rebuild the useful HTML5 portion of #1152 so the built-in `