From db14fba502fb2f92a7c6211415da9d7f72249233 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 3 Aug 2026 09:06:55 +0200 Subject: [PATCH] docs(playback): design recovery recommendations --- ...6-08-03-playback-recommendations-design.md | 482 ++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-03-playback-recommendations-design.md diff --git a/docs/superpowers/specs/2026-08-03-playback-recommendations-design.md b/docs/superpowers/specs/2026-08-03-playback-recommendations-design.md new file mode 100644 index 000000000..b17b82c3f --- /dev/null +++ b/docs/superpowers/specs/2026-08-03-playback-recommendations-design.md @@ -0,0 +1,482 @@ +# 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 content, not the current URL: + +- 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 provider/account plus movie identity; +- episode keys additionally include season and episode identity. + +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.