Files
iptvnator/docs/superpowers/specs/2026-08-03-playback-recommendations-design.md
T
4gray fd96b85c19 feat(playback): recommend recovery actions (#1374)
* docs(playback): design recovery recommendations

* docs(playback): plan recovery recommendations

* refactor(playback): extract diagnostic utilities

* feat(playback): define recovery recommendation contracts

* feat(playback): rank recovery recommendations

* feat(playback): track session recovery attempts

* feat(playback): identify content recovery sessions

* feat(ui): add ranked playback diagnostic panel

* feat(playback): switch temporarily to recommended players

* test(playback): cover temporary player recommendation

* test(playback): verify recommendation capability guards

* docs(playback): document recovery recommendations

* fix(playback): keep recovery keys credential-free

* fix(playback): remove derived tracking ownership

* fix(playback): preserve distinct recovery fallbacks

* fix(playback): reset resume for new sources

* fix(playback): preserve desktop recovery guidance

* docs(playback): clarify recovery policy exceptions

* fix(playback): reject stale progress updates

* fix(playback): keep protected recovery guidance neutral

* test(playback): cover stale progress output

* fix(playback): neutralize protected diagnostic copy

* fix(playback): harden runtime guidance ownership

* fix(playback): stabilize recovery application ownership

* fix(ci): classify playback util coverage

* fix(e2e): preserve playback fixture bytes
2026-08-08 01:04:39 +02:00

491 lines
23 KiB
Markdown

# Playback Recovery Recommendations
## Context
Issue #1159 started as a request for more accurate playback errors and useful
guidance when a stream fails in one player but may work in another. The browser
players now emit structured, fail-closed diagnostics for native media,
hls.js, Video.js/VHS, Shaka Player, and mpegts.js. Those boundaries preserve
public engine evidence without retaining provider messages, credentials, or
arbitrary error payloads.
The remaining recovery policy is still embedded in the UI. Every
`PlaybackDiagnostic` carries an `externalFallbackRecommended` boolean, and
`WebPlayerViewComponent` converts that flag into a fixed MPV/VLC section while
always showing Retry and every available alternative-source row. This cannot
explain when a distinct built-in engine is the better next step, rank recovery
actions by evidence, or avoid recommending a target already tried during the
same playback session.
This design introduces one player-neutral recommendation layer. It ranks a
small set of explicit user actions from structured diagnostic evidence and
runtime capabilities. It does not perform automatic failover.
## Goals
- Convert structured playback diagnostics into deterministic, evidence-based
recovery recommendations.
- Rank one primary recommendation and at most two secondary recommendations.
- Recommend a distinct built-in web engine only when it can plausibly change
the failing playback path.
- Keep MPV/VLC recommendations for source formats and browser restrictions
where an external player can receive the required playback data.
- Keep network, HTTP, and unknown failures fail-closed: prefer Retry or an
alternative source instead of guessing that another decoder will help.
- Let a user temporarily try a recommended built-in player for the current
content without changing the saved player setting.
- Remember attempted targets only for the current in-memory content session so
repeated failures produce a more useful next recommendation.
- Establish a pure playback-domain boundary that future structured native
diagnostics and target capabilities can extend.
## Non-goals
- Automatically switching players or sources.
- Changing the persisted player setting.
- Persistent diagnostic history, cross-session learning, telemetry, or
correlation.
- A retry scheduler, health monitor, or full failover orchestrator.
- Moving diagnostics or recommendation policy into `PlayerController`.
- Replacing the existing multi-source auto-failover behavior.
- Recommending Embedded MPV as an inline target before it emits an equivalent
structured diagnostic lifecycle.
- Detecting whether an external MPV/VLC process ultimately played the stream.
- AirPlay, Cast, Document Picture-in-Picture, or remote-device capabilities.
- Redesigning the diagnostic overlay or the player controls.
## Approaches Considered
### Pure ranked policy layer (selected)
Create a pure playback-domain function that receives structured evidence,
source context, target capabilities, and session-local attempts, then returns
an ordered list of typed recommendations. `WebPlayerViewComponent` remains the
owner of UI state and user-triggered switching.
This keeps classification, policy, and rendering independently testable. It
also makes future evidence sources additive without coupling playback engines
to Angular or turning the controls contract into an error orchestrator.
### Extend diagnostic booleans
Add fields such as `retryRecommended`, `builtInFallbackRecommended`, and
`alternativeSourceRecommended` to `PlaybackDiagnostic` and let the component
choose the buttons. This looks small but duplicates ranking logic, makes
diagnostic producers own runtime/UI policy, and cannot cleanly account for
attempted targets. It is rejected.
### Full failover orchestrator
Introduce a state machine that launches players, observes outcomes, mutates
preferences, and manages retries. This could eventually support automatic
recovery, but it is materially larger and would blur the existing ownership of
source selection, settings, and engine lifecycle. It is rejected for v1.
## Architecture And Ownership
### New playback utility project
Create `libs/playback/util` as the pure playback-domain boundary:
- Nx project name: `playback-util`;
- path alias: `@iptvnator/playback/util`;
- tags: `scope:shared`, `domain:playback`, `type:util`;
- public exports through `libs/playback/util/src/index.ts` only.
The project contains contracts, evidence normalization, diagnostic
classification, source/engine-family mapping, target-capability contracts, and
the recommendation policy. It has no Angular, DOM, storage, settings-store, or
Electron IPC dependency. As a `type:util` project it depends only on other
utility projects, including the existing shared interfaces required for
external-player names and resolved playback metadata.
Move the pure diagnostic model and classifiers from
`libs/ui/playback/src/lib/playback-diagnostics/` into this project. Move only
the pure Shaka evidence/classifier helpers from the Shaka area; the Shaka
engine and video session remain in `ui-playback`. Engine components import the
new alias instead of reaching back into UI-owned diagnostic paths.
For one compatibility window, `@iptvnator/ui/playback` re-exports the public
diagnostic contracts from `@iptvnator/playback/util`. Existing consumers can
therefore migrate without deep imports or a flag day. New code imports the
new alias directly, and the compatibility export can be removed in a later
cleanup PR after all consumers have migrated.
### UI owner
`WebPlayerViewComponent` owns:
- the current content-session key;
- the temporary built-in player override;
- the set of targets attempted in the current session;
- the most recent VOD position used for a best-effort engine handoff;
- the binding generation that rejects stale engine events;
- mapping ranked recommendations to translated, accessible UI;
- existing external-player and alternative-source outputs.
UI-specific translation keys, formatting helpers, Material components, styles,
and diagnostic action cards remain in `libs/ui/playback`.
### Controls contract
`PlayerController` remains the engine-neutral state, command, and capability
contract used by shared controls. Diagnostics and recovery policy stay as a
sibling layer. No recommendation state or commands are added to
`PlayerController`.
## Recommendation Contracts
The utility layer exposes a pure
`recommendPlaybackRecovery(context)` function and a discriminated
recommendation union:
```ts
type PlaybackRecommendation =
| {
readonly action: 'retry';
readonly reason: PlaybackRecommendationReason;
readonly priority: 'primary' | 'secondary';
}
| {
readonly action: 'alternative-source';
readonly reason: PlaybackRecommendationReason;
readonly priority: 'primary' | 'secondary';
}
| {
readonly action: 'player';
readonly target: PlaybackRecommendationTarget;
readonly reason: PlaybackRecommendationReason;
readonly priority: 'primary' | 'secondary';
};
```
`PlaybackRecommendationTarget` is the union of the three built-in diagnostic
players (`videojs`, `html5`, `artplayer`) and the managed external targets
(`mpv`, `vlc`). Embedded MPV is deliberately absent in v1.
Reasons are stable app-owned values, not user-visible prose:
- `retry-transient-failure`;
- `retry-unknown-failure`;
- `alternative-source-available`;
- `different-engine-family`;
- `external-codec-or-container-support`;
- `external-browser-access`;
- `compatible-drm-path`.
Angular maps those values to translations. The model has no numeric confidence
score: the evidence matrix and list order are the contract.
The policy input contains:
- the sanitized `PlaybackDiagnostic`;
- the active target;
- the session-local attempted-target set;
- available target capabilities and their engine family for this source;
- source kind, live/VOD state, and DRM/external-transfer context;
- the count of alternative sources.
The policy output is deterministic, contains at most three entries, and has at
most one primary entry. If the list is non-empty, its first entry is primary
and every later entry is secondary. Current, unavailable, incompatible, and
already attempted targets are excluded before ranking.
`PlaybackDiagnostic.externalFallbackRecommended` is removed. Diagnostic
producers report evidence and classification only; the recommendation policy
decides which actions are safe and useful in the current runtime.
Copy URL and Technical details are utilities rather than recommendations and
are therefore not part of this union.
## Capability And Source Context
The caller supplies explicit capability facts instead of asking the policy to
inspect settings, the DOM, or Electron globals. Each player target records
whether it is available for the current runtime/source and its effective
engine family.
The source context records whether the playback payload can be transferred to
an external player. ClearKey/KODIPROP playback is always non-transferable in
v1: MPV and VLC are excluded because the current external-player request does
not carry an equivalent DRM contract. Header-bearing portal playback is
transferable only when its existing host-specific external launch path forwards
the required resolved playback fields; otherwise the host capability marks it
non-transferable. The policy never infers transferability from an error message
or URL substring.
## Engine-Family Matrix
Recommendations change engines, not merely skins:
| Source path | Built-in engine families | v1 built-in alternative |
| -------------------------------- | ----------------------------------- | ----------------------- |
| HLS in Video.js | Video.js/VHS | HTML5 using hls.js |
| HLS in HTML5 or ArtPlayer | hls.js | Video.js/VHS |
| MPEG-TS in any web player | mpegts.js | None |
| DASH in HTML5 or ArtPlayer | Shaka Player | None |
| DASH in Video.js | not a supported recommendation path | None |
| Native MP4/MKV in any web player | browser media element | None |
Only one target represents a distinct engine family in the ranked list. For a
Video.js HLS failure, HTML5 is the canonical hls.js target; ArtPlayer is not a
second independent engine recommendation. If that target is unavailable or
already attempted, the policy proceeds to an external target rather than
presenting a duplicate engine-family guess.
MPEG-TS does not recommend another built-in player because all three use the
same mpegts.js engine. DASH does not recommend Video.js in v1 because it is not
an equivalent supported Shaka path. Native media does not recommend another
web player because all three ultimately depend on the same browser decoder.
## Policy Matrix
The policy builds the following exact candidate order, then filters unavailable,
current, incompatible, and attempted targets and truncates the result to three
entries. “Alternative source” is omitted when its count is zero.
| Evidence | Ordered candidates | Forbidden guess |
| ----------------------------------------- | --------------------------------------------------------- | -------------------------------- |
| HTTP, timeout, or generic network failure | Retry → Alternative source | Any player change |
| Unknown playback error | Retry → Alternative source | Any player change |
| Browser access/CORS/CSP-class evidence | MPV → VLC → Alternative source | Another browser player |
| Unsupported codec or container | MPV → VLC → Alternative source | Another browser player |
| Media/decode/engine processing failure | Distinct built-in family → MPV → VLC → Alternative source | Same engine family |
| DRM/encryption failure | Compatible built-in path → Alternative source → MPV → VLC | Non-transferable external target |
Network and unknown cases never claim that another decoder is likely to fix
the failure. For DRM, every candidate still passes explicit target and payload
capability checks; ClearKey/KODIPROP therefore never reaches MPV or VLC.
For external actions, MPV precedes VLC to preserve the existing primary
fallback. If MPV is unavailable or attempted, VLC may become primary. When no
ranked recommendation survives, the overlay still exposes Copy URL and
Technical details instead of fabricating a guess.
## Playback Session Lifecycle
### Stable content identity
Every `WebPlayerViewComponent` host supplies a required
`playbackSessionKey`. The key identifies canonical logical content, not the
current URL or selected provider copy:
- an M3U live channel key uses playlist/source identity plus channel identity;
- Xtream and Stalker live keys use provider/account plus content identity;
- movie keys use the owning route/catalog's original source plus movie
identity;
- episode keys use the owning series route's original source and series plus
season and episode identity.
An alternative source's `playback.contentInfo` is provider-scoped playback and
resume metadata, not recovery-session identity. Source-owning route and series
hosts derive the key before passing it through the inline player, so replacing
that playback payload cannot replace the recovery session.
Retry and alternative sources for the same channel, movie, or episode keep the
same key. Selecting a different channel, movie, or episode changes it. A key
change synchronously clears the attempted-target set, temporary override,
handoff position, and visible diagnostic, then advances the binding generation
so every callback from the previous content becomes stale.
The state is component-local, so a same-content source change must retain the
`WebPlayerViewComponent` instance and update its inputs. Destroying that
component ends the recovery session even if a later instance receives the same
key. Host tests enforce retained identity for supported multi-source flows.
### Failure and reranking
When the active web engine emits a terminal diagnostic, the component:
1. verifies that the event belongs to the current binding generation and
active target;
2. records the current target as attempted;
3. stores the diagnostic;
4. emits the existing `playbackFailed` output for source-owner behavior;
5. runs the pure recommendation policy with current capabilities and attempts.
An event from a destroyed or replaced engine is ignored and cannot overwrite
the new session's UI.
### Trying a built-in target
When the user selects a built-in recommendation, the component records that
target as attempted, captures the latest VOD position, clears the diagnostic,
and applies a local override that outranks the host override and saved setting
for this content session. The player host is recreated for the chosen target.
No storage or settings-store mutation occurs.
For VOD and episodes, the new engine receives the latest finite playback
position as a best-effort start time. Live playback restarts at the live edge.
If the target fails, the new diagnostic is reranked with both attempted
targets excluded. If the target proves unavailable before attachment, the
component keeps it attempted and immediately reranks without throwing.
Retry keeps the same content session and attempts, clears the current
diagnostic, and reloads the active target. An alternative-source request also
keeps the same content session and attempts; the source-owning host changes the
resolved playback URL without resetting the recommendation history.
### External targets
MPV/VLC actions continue to emit the existing `PlaybackFallbackRequest` with
the full `ResolvedPortalPlayback` and diagnostic. The selected external target
is recorded as attempted before emission so returning to the overlay can
promote the next useful action. v1 does not infer launch or playback success
and does not persist the result.
## User Interface
Keep the existing diagnostic overlay and its badge, headline, description,
HTTP/container/codec metadata, codec hint, and `role="status"`. This feature
changes the action hierarchy, not the overall visual language.
The overlay renders:
1. one prominent primary recommendation card;
2. at most two compact secondary recommendation cards;
3. the always-available Copy URL and Technical details utilities.
An alternative-source recommendation renders the existing
`VodSourceRow`-based block and occupies one recommendation slot regardless of
the number of visible source rows. Its existing bounded row count and “more
sources” affordance remain.
Retry moves out of the unconditional utility row and appears only when the
policy ranks it. Preserve `playback-retry`, `playback-fallback-mpv`, and
`playback-fallback-vlc`. Built-in actions use
`playback-recommendation-videojs`, `playback-recommendation-html5`, and
`playback-recommendation-artplayer`. Their copy explicitly says that the
change is temporary and does not alter the saved setting.
All actions are native buttons with visible keyboard focus. No autofocus or
focus trap is added to the status overlay. A pending switch disables repeated
activation until the current operation settles. At narrow widths cards stack
vertically, content uses `min-width: 0`, and long translated copy wraps without
forcing horizontal overflow. Existing light/dark application tokens remain
the styling source.
## Error Handling And Safety
- The recommendation function is total and does not throw for missing,
unknown, or future diagnostic evidence.
- Incomplete or contradictory context fails closed to Retry and an available
alternative source; it never upgrades uncertainty into a player claim.
- Unknown engine families cannot produce built-in recommendations.
- Unavailable or non-transferable targets are filtered before rendering.
- A binding generation plus exact target identity rejects stale diagnostic,
async header, and delayed player events after a switch.
- Switching is single-flight from the UI perspective; double clicks cannot
mount two targets or corrupt the attempt set.
- Session state remains memory-only and contains stable target IDs and a
numeric playback position, never raw error payloads or credentials.
- Existing structured-evidence redaction guarantees remain unchanged after
moving the pure files into `playback-util`.
## Testing
Use test-driven development.
### Pure policy and boundary tests
- Add exhaustive table-driven tests in `playback-util` for every policy row,
source engine family, priority transition, availability combination, attempt
exclusion, DRM transfer constraint, and three-result limit.
- Prove network and unknown diagnostics never recommend a player.
- Prove MPEG-TS, native media, and Shaka/DASH never offer a same-engine browser
alternative.
- Prove HLS offers only one distinct built-in engine family.
- Prove ClearKey/KODIPROP excludes MPV/VLC.
- Preserve and migrate all diagnostic/evidence contract tests, including
package-version locks and redaction assertions.
- Add a module-boundary assertion that the new utility project has no Angular,
DOM, Electron, storage, or UI dependency.
### Component and host tests
Extend `WebPlayerViewComponent` tests to cover:
- deterministic primary/secondary rendering and stable test IDs;
- a temporary built-in switch without settings mutation;
- attempted-target exclusion and reranking after another failure;
- Retry and alternative-source preservation of session state;
- complete reset on `playbackSessionKey` change;
- VOD position handoff and live-edge behavior;
- stale generation/target event rejection;
- single-flight action handling and unavailable-target reranking;
- DRM and runtime-capability filtering;
- keyboard focus, disabled state, and narrow-layout structure.
Update the closest M3U, Xtream, Stalker, unified live, and portal inline-player
specs to prove that each host supplies a stable content key and preserves it
while switching sources for the same content.
### E2E coverage
- Add a deterministic web Playwright case backed by repository-owned media
fixtures: trigger an engine-specific HLS failure, assert the distinct
built-in recommendation, activate it, verify the new player host mounts, and
verify the persisted player setting remains unchanged.
- Extend the existing web and Electron DASH/ClearKey E2E cases to prove that
non-transferable DRM never exposes MPV/VLC recommendations.
- Preserve or add an Electron case where an eligible browser/container failure
still exposes the existing managed MPV/VLC actions and emits the expected
external fallback request.
Do not add a production-only diagnostic injection hook for E2E. If a browser
cannot deterministically expose the chosen HLS engine event from a bounded
fixture, the implementation plan must select another public, deterministic
engine event and retain component-level coverage of the exact policy branch.
### Validation ladder
Run at minimum:
- `pnpm nx test playback-util`;
- `pnpm nx lint playback-util`;
- `pnpm nx test ui-playback`;
- `pnpm nx lint ui-playback`;
- focused tests for every host whose session-key binding changes;
- the targeted web and Electron E2E atomized targets discovered from Nx;
- affected application typecheck/build targets;
- `pnpm run i18n:validate`;
- `pnpm run release:notes:validate`;
- Nx module-boundary and project-discovery checks.
The implementation plan must use the exact target names reported by the fresh
workspace rather than inventing commands.
## Documentation And Release Note
Update:
- `docs/architecture/embedded-inline-playback.md` with the canonical
diagnostic-to-recommendation flow and engine-family matrix;
- `docs/architecture/nx-workspace-boundaries.md` with the new
`playback-util` project and alias;
- `AGENTS.md` and `CLAUDE.md` with the new shared playback recommendation
ownership and session behavior.
Update `docs/architecture/player-controls-contract.md` with a short boundary
clarification that recovery recommendations remain outside the controls
contract; the controls API itself does not change.
Add a `fix(playback)` note under `.changes/` because users gain new recovery
actions and temporary built-in player switching. The note describes the user
outcome, not the internal policy extraction.
## Future Extensions
The boundary is intentionally extensible where additional evidence changes a
decision:
- structured Embedded MPV/native-view diagnostics and capabilities;
- explicit external-launch failure results;
- richer per-target codec, container, DRM, and header-transfer capabilities;
- session-local outcome adaptation when a target starts successfully;
- source health facts supplied by an existing source owner.
Persistent history, telemetry-driven ranking, and automatic mutation of player
settings remain low-value or high-risk until a concrete user problem justifies
them.