mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
docs(playback): design shared web picture-in-picture
This commit is contained in:
1 parent
beb62db314
commit
1c3c33f1fc
1 file changed
+266
@@ -0,0 +1,266 @@
|
||||
# Shared Web Picture-in-Picture Design
|
||||
|
||||
## Status
|
||||
|
||||
Selected as the first follow-up to the shared web-player controls rollout.
|
||||
|
||||
## Goal
|
||||
|
||||
Restore Picture-in-Picture (PiP) in IPTVnator's shared controls for the three
|
||||
`<video>`-backed web engines:
|
||||
|
||||
- HTML5/hls.js;
|
||||
- Video.js; and
|
||||
- ArtPlayer.
|
||||
|
||||
The feature must use the same button, accessibility behavior, capability
|
||||
gating, and lifecycle rules in all three engines.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not add PiP, a popup, or an always-on-top mini-player for Embedded MPV.
|
||||
- Do not add AirPlay or Cast.
|
||||
- Do not implement Document Picture-in-Picture.
|
||||
- Do not move IPTVnator's Angular controls into the operating-system PiP
|
||||
window.
|
||||
- Do not add a PiP keyboard shortcut.
|
||||
- Do not change the default-off rollout behavior of shared web controls.
|
||||
- Do not alter the existing vendor/native controls when shared controls are
|
||||
disabled.
|
||||
|
||||
## Architecture decision
|
||||
|
||||
PiP becomes an explicit part of the engine-neutral `PlayerController`
|
||||
contract:
|
||||
|
||||
```ts
|
||||
interface PlayerControlsCapabilities {
|
||||
pictureInPicture: boolean;
|
||||
}
|
||||
|
||||
interface PlayerControlsState {
|
||||
pictureInPictureActive: boolean;
|
||||
canPictureInPicture: boolean;
|
||||
}
|
||||
|
||||
interface PlayerControlsCommands {
|
||||
togglePictureInPicture(): void;
|
||||
}
|
||||
```
|
||||
|
||||
`WebVideoControlsAdapter` owns the standard browser PiP integration because it
|
||||
already owns and rebinds the current `HTMLVideoElement`. This is especially
|
||||
important for Video.js, which can replace its Tech `<video>` after
|
||||
`playerreset`.
|
||||
|
||||
The shared presentation remains independent of HTML5, Video.js, and ArtPlayer:
|
||||
it reads the generic capability/state and invokes the generic command.
|
||||
|
||||
Embedded MPV advertises `pictureInPicture: false`, reports inactive/unavailable
|
||||
state, and implements a no-op command. This is contract compatibility only; no
|
||||
Embedded MPV PiP surface or popup is introduced.
|
||||
|
||||
### Rejected alternatives
|
||||
|
||||
Passing a separate `HTMLVideoElement` input to `app-player-controls` would keep
|
||||
PiP outside the controller, like DOM fullscreen, but would require bespoke
|
||||
target wiring and replacement handling in every web-player host.
|
||||
|
||||
Calling Video.js and ArtPlayer vendor PiP APIs directly would duplicate
|
||||
feature detection, ownership, failure handling, and lifecycle behavior. It
|
||||
would also leave the HTML5 player on a separate implementation.
|
||||
|
||||
## Capability and state semantics
|
||||
|
||||
`pictureInPicture` is true only when all of the following hold:
|
||||
|
||||
- an `HTMLVideoElement` is attached;
|
||||
- `document.pictureInPictureEnabled === true`;
|
||||
- `video.requestPictureInPicture` is a function;
|
||||
- `document.exitPictureInPicture` is a function; and
|
||||
- `video.disablePictureInPicture !== true`.
|
||||
|
||||
`pictureInPictureActive` is true only when
|
||||
`document.pictureInPictureElement === attachedVideo`.
|
||||
|
||||
`canPictureInPicture` is false while an enter/exit operation is pending.
|
||||
Otherwise, it is true when:
|
||||
|
||||
- the attached video is already the active PiP owner, so the user can exit; or
|
||||
- PiP is supported and the video has loaded metadata (`readyState >= 1`,
|
||||
equivalent to `HTMLMediaElement.HAVE_METADATA`).
|
||||
|
||||
Browser `enterpictureinpicture` and `leavepictureinpicture` events are
|
||||
authoritative. The adapter does not optimistically report an active state
|
||||
before the browser confirms entry. Settled promises trigger a reconciliation
|
||||
refresh as a fallback for unusual browser event ordering.
|
||||
|
||||
## Command behavior
|
||||
|
||||
`togglePictureInPicture()` is fire-and-forget, matching the existing command
|
||||
contract:
|
||||
|
||||
- when the attached video owns PiP, call `document.exitPictureInPicture()`;
|
||||
- otherwise, call `video.requestPictureInPicture()`;
|
||||
- ignore the command when capability/readiness gating fails;
|
||||
- ignore an additional toggle while an enter/exit operation is pending;
|
||||
- contain synchronous exceptions and rejected promises; and
|
||||
- never emit an unhandled rejection.
|
||||
|
||||
The request call starts synchronously inside the button click so browser user
|
||||
activation is preserved.
|
||||
|
||||
Fullscreen and PiP remain separate browser-managed presentation modes.
|
||||
IPTVnator does not await one transition before requesting the other, because
|
||||
that can consume the initiating user activation. The adapter and fullscreen
|
||||
helper reconcile the browser's resulting events.
|
||||
|
||||
## Lifecycle and ownership
|
||||
|
||||
The adapter attaches exact `enterpictureinpicture` and
|
||||
`leavepictureinpicture` listeners to its current video alongside the existing
|
||||
media listeners.
|
||||
|
||||
Source replacement on the same video element does not exit PiP. This allows a
|
||||
normal HTML5 or Video.js source change that retains its current video element
|
||||
to continue in the existing PiP window and show the new source. ArtPlayer
|
||||
currently rebuilds its player and video element on channel changes, so that
|
||||
replacement follows the owned-target cleanup below and exits PiP.
|
||||
|
||||
When the adapter detaches or rebinds to a replacement video:
|
||||
|
||||
1. it removes listeners from the old video;
|
||||
2. if and only if the old video currently owns PiP, it best-effort exits PiP;
|
||||
3. it clears local active/pending state;
|
||||
4. it invalidates old asynchronous completions; and
|
||||
5. it binds the replacement video as the only authoritative target.
|
||||
|
||||
An unrelated document PiP element must never be closed by this adapter.
|
||||
Stale events or promise completions from an old target must not mutate the new
|
||||
target's state.
|
||||
|
||||
Video.js Tech replacement therefore exits PiP owned by the old Tech and does
|
||||
not automatically enter PiP on the replacement Tech. Automatic re-entry would
|
||||
require a new user gesture and is intentionally not attempted.
|
||||
|
||||
Destroying HTML5, Video.js, or ArtPlayer performs the same owned-target cleanup.
|
||||
A playback diagnostic hiding shared controls does not itself close PiP; the
|
||||
browser-provided PiP window still has its own close affordance, and target
|
||||
teardown remains the ownership boundary.
|
||||
|
||||
## Shared controls UI
|
||||
|
||||
The PiP action is rendered immediately before fullscreen.
|
||||
|
||||
- It is omitted when `pictureInPicture` is false.
|
||||
- It is disabled while `canPictureInPicture` is false.
|
||||
- `aria-pressed` reflects `pictureInPictureActive`.
|
||||
- Tooltip and accessible text use
|
||||
`EMBEDDED_MPV.PLAYER.ENTER_PICTURE_IN_PICTURE` and
|
||||
`EMBEDDED_MPV.PLAYER.EXIT_PICTURE_IN_PICTURE`.
|
||||
- The inactive and active states use distinct Material icons.
|
||||
- Clicking it reveals the controls and invokes the command exactly once.
|
||||
|
||||
Both translation keys are added to every locale file. No shortcut is added in
|
||||
this change.
|
||||
|
||||
Standard element PiP displays video frames in an OS/browser-managed window. It
|
||||
does not display IPTVnator's Angular control bar or other DOM overlays.
|
||||
Subtitle display inside PiP remains dependent on browser support for the
|
||||
active native text track.
|
||||
|
||||
## Engine behavior
|
||||
|
||||
### HTML5
|
||||
|
||||
The existing shared-controls bridge attaches `WebVideoControlsAdapter` to the
|
||||
component's real `<video>`. No separate PiP host wiring is required.
|
||||
|
||||
### Video.js
|
||||
|
||||
The existing bridge attaches to the current Tech `<video>` and rebinds after
|
||||
`playerreset`. The adapter's owned-target cleanup handles PiP during that
|
||||
replacement. Video.js's vendor PiP button remains available when shared
|
||||
controls are disabled.
|
||||
|
||||
### ArtPlayer
|
||||
|
||||
The existing neutral source bridge attaches to `player.video`. Shared mode
|
||||
keeps ArtPlayer's vendor `pip: false` option so exactly one PiP button is
|
||||
rendered. Legacy mode keeps the existing `pip: true` behavior.
|
||||
|
||||
### Embedded MPV
|
||||
|
||||
Both frame-copy and native-view Embedded MPV remain without PiP. No window,
|
||||
popup, frame-copy routing, or native addon behavior is added.
|
||||
|
||||
## Error handling
|
||||
|
||||
PiP can fail because of missing metadata, browser policy, lost user activation,
|
||||
or an OS-level denial. Such failures:
|
||||
|
||||
- do not change playback state;
|
||||
- do not create persistent error UI;
|
||||
- do not mark PiP active;
|
||||
- clear the pending gate; and
|
||||
- leave the button available for a later user retry when the runtime still
|
||||
reports support.
|
||||
|
||||
## Testing strategy
|
||||
|
||||
### Contract and shared UI
|
||||
|
||||
- defaults keep PiP capability/state false;
|
||||
- the button is absent without capability;
|
||||
- supported-but-not-ready state renders a disabled action;
|
||||
- inactive and active labels/icons/`aria-pressed` are correct; and
|
||||
- a click invokes `togglePictureInPicture()` once.
|
||||
|
||||
### Web adapter
|
||||
|
||||
- feature-detection matrix for document/video APIs and
|
||||
`disablePictureInPicture`;
|
||||
- metadata readiness gating;
|
||||
- request when inactive and exit when active;
|
||||
- authoritative enter/leave event reconciliation;
|
||||
- pending-toggle serialization;
|
||||
- synchronous throw and rejected-promise containment;
|
||||
- listener cleanup;
|
||||
- same-element source refresh preserving active PiP;
|
||||
- active old video to replacement video exits only the old owner;
|
||||
- stale old-video events/completions cannot mutate the replacement; and
|
||||
- detach never exits an unrelated PiP element.
|
||||
|
||||
### Engine integrations
|
||||
|
||||
- HTML5 shared mode exposes web PiP through its attached video;
|
||||
- Video.js Tech replacement rebinds PiP authority and cleans up the old owner;
|
||||
- ArtPlayer shared mode exposes shared PiP while vendor PiP stays disabled;
|
||||
- ArtPlayer channel rebuild exits PiP owned by the replaced video;
|
||||
- all three flag-off paths retain their existing native/vendor chrome; and
|
||||
- Embedded MPV remains capability-false.
|
||||
|
||||
### Validation
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm nx test ui-playback --skip-nx-cache --runInBand
|
||||
pnpm nx lint ui-playback --skip-nx-cache
|
||||
pnpm nx build web --configuration=development --skip-nx-cache
|
||||
```
|
||||
|
||||
Unit tests cannot prove that an OS-managed PiP window opens. Perform a manual
|
||||
smoke test in a supported Chromium/Electron runtime for each web engine:
|
||||
|
||||
1. open a playable stream;
|
||||
2. enter and exit PiP;
|
||||
3. change source on the same element where applicable;
|
||||
4. switch or destroy the player host; and
|
||||
5. confirm the legacy controls path is unchanged.
|
||||
|
||||
## Documentation impact
|
||||
|
||||
Update `docs/architecture/player-controls-contract.md` with the capability,
|
||||
state, command, lifecycle, and browser limitations. Mirror the concise shared
|
||||
controls summary in `AGENTS.md` and `CLAUDE.md`.
|
||||
Reference in new issue
Block a user