From a39a7953c293a1faa52a98f62f1def218b8aba4b Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 4 Aug 2026 17:59:04 +0200 Subject: [PATCH] docs(playback): document recovery recommendations --- .changes/playback-recovery-recommendations.md | 10 ++ AGENTS.md | 25 +++ CLAUDE.md | 29 +++- docs/architecture/embedded-inline-playback.md | 164 +++++++++++++++--- docs/architecture/nx-workspace-boundaries.md | 15 ++ docs/architecture/player-controls-contract.md | 24 ++- 6 files changed, 240 insertions(+), 27 deletions(-) create mode 100644 .changes/playback-recovery-recommendations.md diff --git a/.changes/playback-recovery-recommendations.md b/.changes/playback-recovery-recommendations.md new file mode 100644 index 000000000..477529b25 --- /dev/null +++ b/.changes/playback-recovery-recommendations.md @@ -0,0 +1,10 @@ +--- +type: fix +area: playback +issues: [1159] +--- + +When playback fails, IPTVnator now ranks useful next steps from the reported +error, including another compatible built-in player, MPV/VLC, Retry, or another +source. Trying a built-in recommendation affects only the current item and +does not change the saved player setting. diff --git a/AGENTS.md b/AGENTS.md index 019266cb3..e6dd85e55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -239,6 +239,31 @@ Key files: messages and arbitrary `info` never reach diagnostics. This is a sibling of `PlayerController`, not part of the controls contract. +- Browser playback diagnostics and recovery policy live in + `libs/playback/util` and are exported by `@iptvnator/playback/util`. + Public engine errors cross allowlisted sanitizers into a + `PlaybackDiagnostic`; `recommendPlaybackRecovery(context)` then ranks at + most three actions, and `WebPlayerViewComponent` executes only the action + the user selects. The policy is a sibling of `PlayerController`; shared + controls only gate interaction while the diagnostic panel is visible. + `WebPlayerViewComponent` owns a host-derived content-session key that is + stable for the mounted logical selection, attempted target IDs, the temporary + player override, and VOD handoff position. Its `PlaybackBinding` is exactly + `{ generation, target }`, while + source/header/DRM/application changes use a separate fieldless opaque + `Symbol` token; neither ownership primitive contains URLs, headers, DRM + material, or credentials. A recommended built-in player temporarily + outranks the host override and saved player for that mounted content session, + never mutates `Settings.player`, and resumes finite VOD position on a + best-effort basis; live playback returns to the live edge. Retry and + alternative sources preserve attempts, while a different content-session key + or component teardown resets them. Recovery recommendations never + auto-switch, persist history, learn across sessions, or emit telemetry. + Network and unknown evidence fail closed to Retry/alternative source; PWA + capability suppresses managed MPV/VLC, and ClearKey/KODIPROP DRM suppresses + external targets because its payload is not transferable. Raw engine + messages, arbitrary data, and credentials never enter recommendation + evidence or ownership state. - The built-in HTML5/hls.js player is the second guarded consumer. `HtmlVideoPlayerComponent` provides a component-scoped `WebVideoControlsAdapter`; its neutral `web-video-support` bridge is shared diff --git a/CLAUDE.md b/CLAUDE.md index 93638914e..4ec43157b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -764,8 +764,33 @@ app as a real argument, so it is not an option. validated HTTP 4xx/5xx status; raw messages and arbitrary `info` never reach stored or rendered diagnostics. HTTP/network failures avoid false decoder recommendations, while exact format, codec, truncated-stream, and - MediaSource failures retain actionable fallback guidance. This diagnostic + MediaSource failures retain actionable recovery guidance. This diagnostic layer remains separate from the shared `PlayerController` controls contract. +- Browser playback diagnostics and recovery policy live in + `libs/playback/util` and are exported by `@iptvnator/playback/util`. + Public engine errors cross allowlisted sanitizers into a + `PlaybackDiagnostic`; `recommendPlaybackRecovery(context)` then ranks at + most three actions, and `WebPlayerViewComponent` executes only the action + the user selects. The policy is a sibling of `PlayerController`; shared + controls only gate interaction while the diagnostic panel is visible. + `WebPlayerViewComponent` owns a host-derived content-session key that is + stable for the mounted logical selection, attempted target IDs, the temporary + player override, and VOD handoff position. Its `PlaybackBinding` is exactly + `{ generation, target }`, while + source/header/DRM/application changes use a separate fieldless opaque + `Symbol` token; neither ownership primitive contains URLs, headers, DRM + material, or credentials. A recommended built-in player temporarily + outranks the host override and saved player for that mounted content session, + never mutates `Settings.player`, and resumes finite VOD position on a + best-effort basis; live playback returns to the live edge. Retry and + alternative sources preserve attempts, while a different content-session key + or component teardown resets them. Recovery recommendations never + auto-switch, persist history, learn across sessions, or emit telemetry. + Network and unknown evidence fail closed to Retry/alternative source; PWA + capability suppresses managed MPV/VLC, and ClearKey/KODIPROP DRM suppresses + external targets because its payload is not transferable. Raw engine + messages, arbitrary data, and credentials never enter recommendation + evidence or ownership state. - DASH + ClearKey (M3U module): `.mpd` channels play through a lazily loaded Shaka Player source engine inside the HTML5 and ArtPlayer components (no new player in settings). ClearKey keys come from `#KODIPROP:inputstream.adaptive.*` @@ -917,7 +942,7 @@ engine` (restart required) or helper: `apps/electron-backend/native/helper/`; canonical packaging/runtime contracts: `docs/architecture/embedded-mpv-native.md` and `tools/embedded-mpv/README.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 component-scoped `WEB_PLAYER_SHARED_CONTROLS` rollout token. In fullscreen, `app-player-controls` shows a pointer-transparent media-title overlay at the top while controls are revealed (`mediaTitle` input: movie/channel/series name, plus an `S01E03` second line for episodes; series names flow from the detail views through `PortalInlinePlayerComponent.seriesTitle` and `WebPlayerViewComponent.mediaTitle`). 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/Shaka(DASH)/native tracks, MPEG-TS VOD duration correction, caption preference, and source cleanup. `HtmlVideoElementSession` owns native video-event lifecycle, persisted volume, and start-time/time/ended propagation. 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/DASH(Shaka)/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/Shaka/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. `Settings.showCaptions` is deliberately outside this rollout gate: it is engine state, so the preference-off players apply it through the same helpers without an adapter (`WebVideoSourceTracks` for HTML5/ArtPlayer, `VjsLegacyTracks` for Video.js), re-applying it as the engine adds or switches text tracks. The two modes differ in how long it is enforced: shared controls are authoritative for the session (user intent arrives via `setSubtitleTrack`), while vendor chrome is source-default — the preference seeds each new source and is released once the media reports `playing`, so the engine's own caption menu keeps working. Mode selection is the optional `playbackStarted` probe the legacy owners pass to all three helpers (HLS, native text tracks, Shaka); in that mode the HLS helper deselects (`subtitleTrack = -1`) rather than hiding, since `subtitleDisplay` would override the vendor menu, and DASH is seeded by `ShakaVideoSession.start()` after the manifest loads. `WebPlayerViewComponent` reads it from `SettingsStore` instead of a host input so every host (M3U, Xtream/Stalker live layouts, portal detail inline player) inherits it. 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 component-scoped `WEB_PLAYER_SHARED_CONTROLS` rollout token. In fullscreen, `app-player-controls` shows a pointer-transparent media-title overlay at the top while controls are revealed (`mediaTitle` input: movie/channel/series name, plus an `S01E03` second line for episodes; series names flow from the detail views through `PortalInlinePlayerComponent.seriesTitle` and `WebPlayerViewComponent.mediaTitle`). 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/Shaka(DASH)/native tracks, MPEG-TS VOD duration correction, caption preference, and source cleanup. `HtmlVideoElementSession` owns native video-event lifecycle, persisted volume, and start-time/time/ended propagation. 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/DASH(Shaka)/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/Shaka/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 ranked recovery actions remain visible. On the preference-off path, all three web players retain their existing controls, source behavior, and legacy series navigation. `Settings.showCaptions` is deliberately outside this rollout gate: it is engine state, so the preference-off players apply it through the same helpers without an adapter (`WebVideoSourceTracks` for HTML5/ArtPlayer, `VjsLegacyTracks` for Video.js), re-applying it as the engine adds or switches text tracks. The two modes differ in how long it is enforced: shared controls are authoritative for the session (user intent arrives via `setSubtitleTrack`), while vendor chrome is source-default — the preference seeds each new source and is released once the media reports `playing`, so the engine's own caption menu keeps working. Mode selection is the optional `playbackStarted` probe the legacy owners pass to all three helpers (HLS, native text tracks, Shaka); in that mode the HLS helper deselects (`subtitleTrack = -1`) rather than hiding, since `subtitleDisplay` would override the vendor menu, and DASH is seeded by `ShakaVideoSession.start()` after the manifest loads. `WebPlayerViewComponent` reads it from `SettingsStore` instead of a host input so every host (M3U, Xtream/Stalker live layouts, portal detail inline player) inherits it. 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 diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index cfa3ad393..ff4cd6820 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -19,7 +19,8 @@ This document records the current contract for embedded playback in portal detai saved episode playback positions. - Embedded playback UI is always hosted by the current view. `PlayerService` launches MPV/VLC only and does not open an embedded-player dialog. -- Browser-player failures are diagnosed client-side and can offer explicit MPV/VLC fallback actions without changing the saved player setting. +- Browser-player failures are diagnosed client-side and produce ranked, + user-triggered recovery actions without changing the saved player setting. ## Scope @@ -64,6 +65,13 @@ content or episode change must produce a different key. The serialized key is created with `createPlaybackSessionKey()` from `@iptvnator/playback/util` so delimiter-bearing provider IDs remain unambiguous. +The key is host-owned and stable only for the logical selection represented by +that mounted host. In particular, M3U identity uses the current playlist/source +identity and `Channel.id`; it does not claim durability across a refresh that +replaces either identity. Recovery state contains this credential-free key, +target IDs, generation counters, and a finite VOD resume position. It never +uses a playback URL, headers, DRM configuration, or credentials as identity. + Inline hosts capture this identity before asynchronous playback resolution. A completion may mount only while the same owner is current; stale completions and embedded starts without a complete canonical identity are ignored without @@ -262,9 +270,9 @@ Embedded playback does not have a fallback dialog path. `PlayerService.openResolvedPlayback(...)` remains the MPV/VLC external launch entry point; for embedded players it returns without creating UI. -Diagnostics and fallback UI: +Diagnostics, recovery policy, and recovery UI: -- `/Users/4gray/Code/iptvnator/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.ts` +- `/Users/4gray/Code/iptvnator/libs/playback/util/src/index.ts` - `/Users/4gray/Code/iptvnator/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts` ## Playback Decision Rule @@ -340,7 +348,30 @@ Current contract: ## Codec And Container Diagnostics -The shared `WebPlayerViewComponent` is the central browser-player viewport for M3U, Xtream, and Stalker inline playback, including live streams opened from favorites and recently viewed collections. Video.js, HTML5, and ArtPlayer report native media errors, HLS.js errors, mpegts.js errors, and HLS manifest codec metadata into the shared diagnostics classifier. +The shared `WebPlayerViewComponent` is the central browser-player viewport for +M3U, Xtream, and Stalker inline playback, including live streams opened from +favorites and recently viewed collections. Video.js, HTML5, and ArtPlayer +report native media errors, hls.js errors, Video.js/VHS errors, Shaka errors, +mpegts.js errors, and HLS manifest codec metadata into the DOM-free classifiers +exported by `@iptvnator/playback/util`. + +The canonical recovery flow is: + +```text +engine public error + -> sanitized PlaybackDiagnostic (@iptvnator/playback/util) + -> recommendPlaybackRecovery(context) + -> ranked maximum-three action model + -> WebPlayerView session-local user action +``` + +Engine adapters own public-event collection and sanitation. The playback +utility owns diagnostic contracts, evidence classification, source/engine +mapping, capability contracts, and the pure recommendation policy. +`WebPlayerViewComponent` owns only the current session state and execution of a +user-selected action. Diagnostic producers do not decide which player to show, +and the recommendation policy does not inspect Angular, the DOM, settings, +storage, or Electron globals. The diagnostics remain client-only: @@ -449,10 +480,10 @@ or media cause, so stage and failure remain unknown. A failed public `Player.isBrowserSupported()` preflight is not a Shaka error and therefore retains fully unknown technical evidence instead of being -mislabelled as an unsupported container. For clear DASH, the diagnostic still -offers configured MPV/VLC actions because the failure is specific to the web -engine. KODIPROP DRM sources keep external fallback disabled because external -players do not receive their key configuration. +mislabelled as an unsupported container. Clear DASH can still rank configured +MPV/VLC actions when the structured diagnostic and runtime capabilities permit +them. KODIPROP DRM sources never rank external players because the external +launch contract does not transfer their key configuration. Shaka messages, URLs, headers, request/response bodies, credentials, license/key payloads, and arbitrary `error.data` objects are neither retained @@ -474,7 +505,8 @@ status from the top-level `info.code` slot of Exact public pairs classify HTTP/timeout/exception as network failures, `FormatUnsupported` as an unsupported container, `CodecUnsupported` as an unsupported codec, and `FormatError`/`MediaMSEError` as media failures. -`UnrecoverableEarlyEof` remains a fallback-actionable `media-decode-error`: +`UnrecoverableEarlyEof` remains a `media-decode-error` that may rank a distinct +engine or external player: mpegts.js has already exhausted its internal finite-source early-EOF recovery, and another demuxer may tolerate the truncated transport stream. Mismatched or unknown pairs fail closed to `unknown-playback-error`. @@ -485,17 +517,20 @@ prove CORS, mixed content, CSP, or private-network access, so mpegts.js no longer creates `browser-access-error` from message text. HTTP and other network failures do not claim an external decoder will fix the provider response; container, codec, truncated-stream, format, and MediaSource failures retain -the existing explicit MPV/VLC fallback behavior. +evidence that can rank an explicit MPV/VLC action when the payload and runtime +permit it. The diagnostic surface covers the inline player viewport when playback fails, -with a compact warning badge, a native-player fallback headline, and -player-card actions for configured external players. It exposes technical -details on demand: diagnostic code, reporting player/source, detected -container/MIME, video/audio codecs, native browser error fields, sanitized -structured Video.js/VHS, HLS, Shaka, and mpegts.js evidence. HLS -manifest codec metadata also drives a concise browser-support hint for codecs -that Chromium/Electron commonly cannot decode inline, such as HEVC, AC-3, -E-AC-3, DTS, and MPEG-2 video. +with a compact warning badge, one primary recommendation, at most two secondary +recommendations, and always-available Copy URL and Technical details utilities. +An alternative-source recommendation consumes one of those three slots even +when its bounded source list renders several rows. Technical details contain +the diagnostic code, reporting player/source, detected container/MIME, +video/audio codecs, native browser error fields, and sanitized structured +Video.js/VHS, HLS, Shaka, and mpegts.js evidence. HLS manifest codec metadata +also drives a concise browser-support hint for codecs that Chromium/Electron +commonly cannot decode inline, such as HEVC, AC-3, E-AC-3, DTS, and MPEG-2 +video. URL extension metadata is filtered before diagnostics and player selection use it. Web script extensions such as `.php` are not shown as stream containers; explicit media query metadata such as `extension=ts` or `format=m3u8` is preferred when present. @@ -503,14 +538,99 @@ MKV sources are attempted through Chromium's native Matroska path. Video.js receives `video/matroska` for `.mkv` URLs and explicit query metadata such as `extension=mkv` or `container=mkv`; ArtPlayer and HTML5 continue to use their native video paths. This is container support rather than a universal codec -guarantee: native source or decode failures still produce the existing -diagnostic and explicit MPV/VLC fallback. +guarantee: native source or decode failures still produce a diagnostic whose +ranked actions may include MPV/VLC. Portal VOD and episode payloads with `contentInfo` are treated as non-live by the inline players unless `isLive` is explicitly set. If Chromium leaves the underlying MediaSource duration at `Infinity` for a finite TS VOD, the Video.js wrapper normalizes its UI duration from the finite `seekable` or `buffered` range. Embedded MPV uses the same live decision rule and shows an unknown duration placeholder for VOD/episode snapshots until MPV reports a finite duration. This removes the misleading `LIVE` control state without changing stream decoding, diagnostics, or external fallback behavior. -When a diagnostic is actionable in Electron, the diagnostic surface may offer `Open in MPV`, `Open in VLC`, `Copy URL`, technical details, and `Retry`. Web builds only expose copy/help text and retry. MPV/VLC fallback requests carry the original `ResolvedPortalPlayback` payload so headers, referer, origin, user-agent, content metadata, and resume offset stay intact. Retry clears the current diagnostic and rebuilds the active inline player inputs; it does not change the saved player setting. +## Recovery Recommendation Policy -`PortalPlayer.openExternalPlayback(playback, player)` is the forced external launch API. It sends the playback payload to MPV or VLC regardless of the current saved player setting, so fallback buttons do not mutate preferences. +Recommendations change playback paths only when structured evidence supports +that conclusion. A different skin over the same engine family is not a distinct +recovery target. + +| Source path | Active engine family | Distinct built-in recommendation | +| ---------------------------------------- | ------------------------------- | -------------------------------- | +| HLS in Video.js | Video.js/VHS | HTML5 through hls.js | +| HLS in HTML5 or ArtPlayer | hls.js | Video.js/VHS | +| MPEG-TS in any web player | shared mpegts.js | None | +| DASH in HTML5 or ArtPlayer | Shaka | None | +| DASH in Video.js | unsupported recommendation path | None | +| Native media/container in any web player | browser native-media | None | + +HTML5 is the canonical hls.js alternative to Video.js/VHS; ArtPlayer is not a +second independent hls.js choice. MPEG-TS, DASH/Shaka, and native media never +offer a same-family built-in alternative. Unknown source or engine-family facts +also suppress built-in recommendations. + +The pure policy builds the following order, filters the current, unavailable, +incompatible, and already attempted targets, and then returns at most three +actions. The first surviving action is primary and later actions are secondary. + +| Sanitized evidence | Candidate order | +| ----------------------------------------- | ------------------------------------------------------------ | +| HTTP, timeout, or generic network failure | Retry -> Alternative source | +| Unknown playback error | Retry -> Alternative source | +| Browser access/CORS/CSP-class failure | MPV -> VLC -> Alternative source | +| Unsupported codec or container | MPV -> VLC -> Alternative source | +| Media/decode/engine processing failure | Distinct built-in family -> MPV -> VLC -> Alternative source | +| DRM/encryption failure | Compatible built-in path -> Alternative source -> MPV -> VLC | + +Network and unknown evidence fail closed: they never claim that changing a +decoder will repair the provider response. Contradictory or incomplete +capability facts fail closed to Retry and an available alternative source. +External targets require both managed external-player support and a transferable +payload. PWA builds therefore never rank MPV/VLC. Any ClearKey/KODIPROP DRM +payload is non-transferable and suppresses both external targets; the policy +does not infer transferability from a message or URL. Eligible Electron portal +fallback requests keep using the original `ResolvedPortalPlayback`, so the +existing host path can forward its required headers and playback metadata. + +## Recovery Session Lifecycle And Privacy + +Each mounted `WebPlayerViewComponent` owns one in-memory recovery session for +its required `playbackSessionKey`. Retry and an alternative source for the same +logical content keep the key and attempted-target set. A different channel, +movie, or exact episode changes the key and synchronously clears the diagnostic, +attempts, temporary player override, and handoff position. Destroying the +component also ends the session; the same key in a later component is a new +session. + +On a terminal failure, the current binding is accepted only when both its +generation and inline target still match. The current target becomes attempted, +the sanitized diagnostic is stored, and recommendations are reranked. The +`PlaybackBinding` contains exactly `{ generation, target }`. Changes to the +playback URL, headers, DRM, live/VOD mode, target, or reload generation are +instead correlated with a fieldless opaque `Symbol` application token. Neither +object embeds source material, and recovery ownership state never stores URLs, +headers, DRM keys, error payloads, or credentials. + +Selecting a built-in recommendation records the target, clears the diagnostic, +and installs a temporary local override ahead of the host override and saved +player setting. The new engine receives the latest finite VOD position as a +best-effort resume point; live playback starts at the live edge. Retry reloads +the active target without clearing attempts. Selecting MPV or VLC records the +external target before emitting the existing fallback request. The system does +not infer whether the external process ultimately played the stream. + +No recommendation mutates `Settings.player` or another persisted setting. +Recovery recommendations never auto-switch a player or source and do not +replace the separate source-owner auto-failover feature. Attempts, overrides, +resume handoff, and diagnostics are session-local: there is no persistent +history, cross-session learning, correlation, or telemetry. + +Only allowlisted public engine evidence crosses the structured sanitizers. +Provider/engine messages, request and response objects, arbitrary `error.data` +or `info`, bodies, URLs copied from error payloads, headers, DRM material, and +credentials do not enter recommendation evidence or recovery ownership state. +The active playback URL remains available only through the pre-existing +playback metadata needed by Retry, Copy URL, and eligible explicit +external-player actions. + +`PortalPlayer.openExternalPlayback(playback, player)` remains the forced +external launch API. It sends the resolved playback payload to MPV or VLC +regardless of the current saved player setting, so a recommendation never +mutates preferences. ## External Player Arguments diff --git a/docs/architecture/nx-workspace-boundaries.md b/docs/architecture/nx-workspace-boundaries.md index 9d58fc9c6..8304d69e3 100644 --- a/docs/architecture/nx-workspace-boundaries.md +++ b/docs/architecture/nx-workspace-boundaries.md @@ -78,6 +78,21 @@ in `libs/ui/playback`, while DOM-free diagnostic contracts and classifiers live in `libs/playback/util` and receive browser capability checks as explicit probes. +`libs/playback/util` is the `playback-util` Nx project and is imported through +`@iptvnator/playback/util`. Its exact tags are `scope:shared`, +`domain:playback`, and `type:util`. It owns the public playback diagnostic, +structured engine-evidence, source/engine-family, target-capability, +content-session-key, and recovery-recommendation contracts and pure helpers. +Its public API is `libs/playback/util/src/index.ts`. + +`playback-util` has no Angular, DOM, settings, storage, UI, or Electron IPC +ownership. Browser/player adapters collect public engine events and supply +explicit capability facts; `playback-util` classifies and ranks them without +inspecting runtime globals. As a `type:util` project it may depend only on +other utility projects, including shared interface contracts, while +`ui-playback` and feature hosts may depend on it to render and execute +session-local recovery actions. + ## Project Tags Every Nx project keeps one tag from each family in `project.json`: diff --git a/docs/architecture/player-controls-contract.md b/docs/architecture/player-controls-contract.md index 8fc5d161f..323af7f7b 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 supports four runtime -consumers and includes: +The shared-controls foundation supports four runtime consumers and includes: - the `PlayerController` contract, default state, and capability presets; - the standalone `app-player-controls` presentation component and its @@ -82,6 +81,25 @@ also includes a recording coordinator that correlates asynchronous snapshots with the active playback/session owner, serializes toggles, and cancels pending ownership when the session, playback, engine, or component changes. +## Diagnostics And Recovery Ownership Boundary + +`PlayerController` remains a sibling of playback diagnostics and recovery +recommendations. It owns engine-neutral playback state, capabilities, and +commands for the shared controls; it does not classify errors, call +`recommendPlaybackRecovery()`, rank actions, track recovery attempts, choose a +temporary player, or own content-session policy. + +Those pure contracts and policies live in `@iptvnator/playback/util` (Nx +project `playback-util`). `WebPlayerViewComponent` owns their in-memory, +session-local application. No diagnostic or recommendation state or command is +added to `PlayerController`. + +The only controls-layer participation is interaction gating. While the sibling +diagnostic panel is visible, a web-player host disables shared surface and +keyboard ownership and exits only its own DOM fullscreen so the recovery +actions remain reachable. Clearing the diagnostic restores those paths; it +does not make the controls contract an owner of the recovery lifecycle. + ## Why this exists Historically each playback engine owned both media integration and controls UI. @@ -306,7 +324,7 @@ playback diagnostic is visible: `WebPlayerViewComponent` passes components bind that value to `showControls` and `shortcutsEnabled`. If the active player shell owns DOM fullscreen, its host exits fullscreen before hiding the controls so the sibling diagnostic banner -and its retry/fallback actions remain visible; fullscreen owned by another +and its recovery actions remain visible; fullscreen owned by another element is left untouched. Retrying playback or clearing the diagnostic restores both interaction paths.