docs(playback): document shared web picture-in-picture

This commit is contained in:
4gray committed 2026-07-17 20:16:29 +02:00
1 parent f5a97d36ff
commit 50b8518c9e
3 files changed
+105 -2

No files matched your search

+20
View File
@@ -191,6 +191,26 @@ Key files:
double-click ownership. Diagnostic interaction gating and owned-fullscreen
exit match the other web players. The preference-off path keeps the legacy
ArtPlayer skin, source behavior, and series navigation unchanged.
- Shared web picture-in-picture stays inside that default-off rollout.
`PlayerController` exposes capability `pictureInPicture`, state
`pictureInPictureActive`/`canPictureInPicture`, and command
`togglePictureInPicture()`. HTML5, Video.js, and ArtPlayer use standard
element PiP from the adapter's attached video; shared ArtPlayer keeps vendor
`pip: false`, while preference-off native/vendor paths remain unchanged. The
capability-gated button sits before fullscreen and uses active enter/exit
semantics; entry is disabled until metadata, and the action is disabled while
an operation is pending. Embedded MPV reports capability/state false with a
no-op command and has no popup/mini-window.
- `WebVideoControlsAdapter` reads the attached video and its `ownerDocument`;
browser enter/leave events are authoritative, and exact-owner exit remains
available if request support changes. Request/exit invocation stays
synchronous for user activation, one operation is serialized, and binding
generation plus exact video identity protects replacement and teardown from
stale completion. Video.js Tech reset and ArtPlayer rebuild rebind with
exact-owner cleanup; HTML5 source changes on a retained target preserve PiP.
Standard PiP shows the browser/OS video surface without Angular control
chrome, with browser-dependent subtitles. AirPlay, Cast, Document PiP, a PiP
keyboard shortcut, and Embedded MPV popup/native support are out of scope.
- Canonical docs: `docs/architecture/player-controls-contract.md` and
`docs/architecture/embedded-mpv-native.md`
+20
View File
@@ -619,6 +619,26 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use
- 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=<x11-window>` 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 `<canvas data-embedded-mpv-frame>`, 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 component-scoped `WEB_PLAYER_SHARED_CONTROLS` rollout token. Persisted `Settings.webPlayerSharedControls` is default-off, and its checkbox appears only when HTML5, Video.js, or ArtPlayer is selected. `WebPlayerViewComponent` snapshots the preference into the immutable token for each new player host. The parent `/workspace` route awaits the initial `SettingsStore` load, including cold-start direct links, before this snapshot can occur. Saving applies to the next host without an application restart; an existing session never changes controls mode in place. Embedded MPV ignores the web-player preference: frame-copy always uses shared DOM controls through `EmbeddedMpvControlsAdapter`, native-view retains its compositor-safe legacy dock, and external MPV/VLC retain their own UI. The Embedded MPV host selects exactly one controls UI for its reported engine. `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 neutral `web-video-support` bridge is shared with ArtPlayer and 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. Video.js is the third guarded consumer: `VjsPlayerComponent` provides a component-scoped `WebVideoControlsAdapter`; its bridge rebinds the current Tech video after `playerreset`, exposes source-stable audio/subtitle IDs, preserves caption preference and explicit subtitle-off state, and reads Video.js duration. Reset-driven raw MPEG-TS changes pause first, coalesce to the latest desired source, preserve actual volume across Video.js's reset, and restart when authoritative live/VOD metadata changes. In shared-controls mode, Video.js native controls, click/double-click/hotkey actions, and spatial navigation are disabled. ArtPlayer is the fourth guarded consumer: `ArtPlayerComponent` provides a component-scoped `WebVideoControlsAdapter`; `ArtPlayerSourceSession` owns HLS/MPEG-TS/native sources, the neutral web-video bridge, exact cleanup, and a destroyed-session guard for delayed `customType` callbacks, while `ArtPlayerVideoSession` owns native media/ArtPlayer events. Shared ArtPlayer mode uses authoritative live/VOD metadata, HLS/native tracks and caption preference, MPEG-TS VOD duration correction, and reapplies app volume directly after ArtPlayer restores its own stored volume. Vendor chrome/hotkeys are disabled, and a transparent capture layer gives shared controls exclusive click and double-click ownership. `WebPlayerViewComponent.resolvedIsLive` supplies authoritative metadata; visible playback diagnostics disable shared pointer/keyboard ownership and exit only the active HTML5, Video.js, or ArtPlayer shell's own fullscreen so retry/fallback actions remain visible. On the preference-off path, all three web players retain their existing controls, source behavior, and legacy series navigation. Contract: `docs/architecture/player-controls-contract.md`.
- Shared web picture-in-picture stays inside that default-off rollout.
`PlayerController` exposes capability `pictureInPicture`, state
`pictureInPictureActive`/`canPictureInPicture`, and command
`togglePictureInPicture()`. HTML5, Video.js, and ArtPlayer use standard
element PiP from the adapter's attached video; shared ArtPlayer keeps vendor
`pip: false`, while preference-off native/vendor paths remain unchanged. The
capability-gated button sits before fullscreen and uses active enter/exit
semantics; entry is disabled until metadata, and the action is disabled while
an operation is pending. Embedded MPV reports capability/state false with a
no-op command and has no popup/mini-window.
- `WebVideoControlsAdapter` reads the attached video and its `ownerDocument`;
browser enter/leave events are authoritative, and exact-owner exit remains
available if request support changes. Request/exit invocation stays
synchronous for user activation, one operation is serialized, and binding
generation plus exact video identity protects replacement and teardown from
stale completion. Video.js Tech reset and ArtPlayer rebuild rebind with
exact-owner cleanup; HTML5 source changes on a retained target preserve PiP.
Standard PiP shows the browser/OS video surface without Angular control
chrome, with browser-dependent subtitles. AirPlay, Cast, Document PiP, a PiP
keyboard shortcut, and Embedded MPV popup/native support are out of scope.
**VOD/Series Detail Pages (two-state layout)**:
+65 -2
View File
@@ -14,6 +14,8 @@ consumers and includes:
- the standalone `app-player-controls` presentation component and its
transient-state collaborators;
- a generic `WebVideoControlsAdapter` plus small host helpers;
- standard element picture-in-picture through that adapter for the guarded web
consumers;
- a persisted, default-off web-player preference resolved through an immutable
per-host rollout token;
- the component-scoped `EmbeddedMpvControlsAdapter`;
@@ -60,6 +62,12 @@ remove exact listeners and engines, and destroyed sessions ignore stale delayed
`customType` callbacks. When the host token resolves to false, the existing
ArtPlayer skin, source behavior, and legacy series navigation remain unchanged.
With shared controls enabled, HTML5, Video.js, and ArtPlayer expose standard
element picture-in-picture through the adapter's attached `<video>`. Shared
ArtPlayer keeps its vendor `pip` option disabled so the shared button is the
only PiP owner. The preference-off native/vendor paths remain unchanged.
Embedded MPV advertises no PiP capability and its command is a no-op.
`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
@@ -147,7 +155,7 @@ interface PlayerController {
`PlayerControlsCapabilities` contains booleans for `seek`, `volume`,
`audioTracks`, `subtitles`, `playbackSpeed`, `aspectRatio`, `recording`,
`fullscreen`, and `seriesNavigation`.
`pictureInPicture`, `fullscreen`, and `seriesNavigation`.
The default is all-false. An adapter enables only features that its engine and
current runtime support. Capability flags primarily control whether optional UI
@@ -163,7 +171,8 @@ is rendered; state such as `canSeek`, `canPreviousEpisode`, and
- volume;
- pre-labelled audio/subtitle tracks and subtitle-enabled state;
- playback speed and aspect-ratio selections/presets;
- recording state; and
- recording state;
- picture-in-picture active state and runtime availability; and
- previous/next episode availability.
Adapters translate engine types into this model. The controls component must not
@@ -184,6 +193,7 @@ owner.
- `setPlaybackSpeed`
- `setAspectRatio`
- `toggleRecording`
- `togglePictureInPicture`
Episode navigation is deliberately exposed as component outputs
(`previousEpisodeRequested` and `nextEpisodeRequested`) because the owning
@@ -305,6 +315,53 @@ 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.
### Standard element picture-in-picture
Picture-in-picture is part of the existing default-off shared web-controls
rollout. It is available through standard element PiP for HTML5, Video.js, and
ArtPlayer only when their host snapshot enables `WEB_PLAYER_SHARED_CONTROLS`.
The preference-off HTML5 native controls, Video.js skin, and ArtPlayer vendor
controls keep their previous behavior. Shared ArtPlayer explicitly keeps vendor
`pip: false`, leaving the shared action as the single PiP owner.
The contract exposes:
- capability `pictureInPicture`;
- state `pictureInPictureActive` and `canPictureInPicture`; and
- command `togglePictureInPicture()`.
The shared button renders only when the capability is present, immediately
before fullscreen. It uses the active state for pressed, icon, and enter/exit
semantics. When inactive, entry requires
`readyState >= HTMLMediaElement.HAVE_METADATA`; when active, exact-owner exit
remains available regardless of entry readiness or request support, provided
the exit API exists. Any pending PiP operation disables the action.
`WebVideoControlsAdapter` reads standard PiP APIs from the actual attached
`HTMLVideoElement` and its `ownerDocument`. Browser
`enterpictureinpicture`/`leavepictureinpicture` events and the document's exact
`pictureInPictureElement` remain authoritative; command completion never
optimistically changes the active state.
The adapter invokes `requestPictureInPicture()` or `exitPictureInPicture()`
synchronously from `togglePictureInPicture()` so browser user activation is
preserved, then contains asynchronous settlement. Only one enter/exit operation
may be pending. A binding generation plus exact video identity prevents a stale
completion from clearing or changing the new binding. Replacement or teardown
exits PiP only when the old video is the document's exact owner; a stale
successful entry receives the same exact-owner cleanup and never exits an
unrelated PiP element.
Video.js Tech reset and ArtPlayer video rebuild paths detach the old binding,
perform exact-owner cleanup, and bind the replacement video. HTML5 source
changes on a retained video target, along with ordinary same-element
source/media events, preserve active PiP.
Standard element PiP displays the browser/OS video surface, not Angular shared
control chrome. Subtitle rendering in that surface is browser-dependent.
AirPlay, Cast, Document Picture-in-Picture, a PiP keyboard shortcut, and an
Embedded MPV popup or native mini-window are out of scope.
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,
@@ -414,6 +471,11 @@ 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.
`EmbeddedMpvControlsAdapter` reports `pictureInPicture: false`,
`pictureInPictureActive: false`, and `canPictureInPicture: false`;
`togglePictureInPicture()` is a no-op. Neither renderer opens an MPV
popup/mini-window.
### Frame-copy engine
The experimental frame-copy engine uploads helper-produced frames to
@@ -489,6 +551,7 @@ libs/ui/playback/src/lib/player-controls/
├── web-player-controls.flag.ts
├── web-video-controls.adapter.ts
├── web-video-controls.host.ts
├── web-video-controls.media-helpers.ts
└── index.ts
```