From b87c88833be48d03f9fe217a01ff33aca7816755 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 4 Oct 2026 09:19:32 +0200 Subject: [PATCH] docs(portals): document forced MPV/VLC launch and pending-start contracts The rules #1792 settled for forced external launches and playback-start bookkeeping lived only in code comments and specs. Record them as contracts in the owning documents: - embedded-inline-playback.md: the shared detail-host rules (one external player per title, unconfirmed teardown cancels, ownership rechecked after every await, owner-scoped pending state). - xtream-portal-compatibility.md: the series launch chain, page-token duplicate guard and queued choice. - vod-multi-source.md: the movie menu launch and reset follow the copy the primary button acts on; pending resets are a list of targets. - stalker-portal.md: the per-series launch queue, the batch-held choice and the movie launch/reset pending start. Co-Authored-By: Claude Fable 5.1 --- docs/architecture/embedded-inline-playback.md | 45 +++++++++++++++ docs/architecture/stalker-portal.md | 57 +++++++++++++++++++ docs/architecture/vod-multi-source.md | 24 ++++++++ .../xtream-portal-compatibility.md | 36 ++++++++++++ 4 files changed, 162 insertions(+) diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index 38751413c..1f65b2b51 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -468,6 +468,51 @@ position telemetry overwrites this launch marker when available. This keeps the last-watched season and episode correct even when an external player's progress interface is unavailable; exact external timestamps remain best-effort. +## Forced External Launches From Detail Pages + +The detail "…" menu's "Open in external player" sends the title to MPV/VLC +through `PortalPlayer.openExternalPlayback(playback, player)` whatever the +configured player is. The launch IPC cannot be cancelled, and Electron +publishes the session only once it resolves, so every detail host keeps these +rules: + +- **One external player per title.** Before launching, the host closes the + external session it owns for that title. The `owns` predicate names only the + page's own content; any other session is left alone. With instance reuse + off, a second detached player would otherwise start beside the first. + Stalker hosts use `replaceOwnedExternalSession` from + `@iptvnator/portal/shared/util`; the Xtream pages use the equivalent + `closeRunningExternalSession`. +- **Unconfirmed teardown cancels the launch.** A live session without a + closer, or a close that rejects, leaves the running player in place and + nothing new launches. +- **Ownership is rechecked after every await.** Stream resolution, the close + and the launch IPC can each outlive the page or be superseded by a newer + start. A stale step stops without reporting, and a launch that resolves + stale closes the session it just opened. +- **No second player while a launch settles.** A repeat of the same launch is + ignored, or its control stays disabled. Movie pages refuse or disable every + other start of that title until the launch settles. Series pages hold the + latest episode choice and, once the launch settled, replace the player it + opened, only while that series is still on screen. +- **Pending state is owner-scoped.** A start still resolving, or a progress + reset still writing, holds the actions of its own title only: another title + shown by the reused page is not blocked by it. Movie hosts track starts with + `createPendingPlaybackStart` (`@iptvnator/portal/shared/util`): only the + latest start may clear the flag, and `isPendingFor(owner)` answers for one + owner. Stalker hosts, whose starts wait on a portal round trip, also + `retire(owner)` when the selection leaves it, so a request that never + settles cannot hold a return to the same title. The one page-wide gate is + the Xtream movie page's external launch that has not settled yet. + +Owner keys and queueing are provider contracts: + +| Host | Contract | +| --- | --- | +| Xtream series | [Forced external launches from detail pages](./xtream-portal-compatibility.md#forced-external-launches-from-detail-pages) | +| Xtream movie | [Menu launch and reset follow the primary button](./vod-multi-source.md#menu-launch-and-reset-follow-the-primary-button) | +| Stalker series and movies | [Forced External Launches](./stalker-portal.md#forced-external-launches) | + ## Series Quick Start CTA Xtream and Stalker series detail views share the quick-start decision helper in diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md index d39c1f61b..3f7da7ed0 100644 --- a/docs/architecture/stalker-portal.md +++ b/docs/architecture/stalker-portal.md @@ -1558,6 +1558,63 @@ Core decision logic and normalization are centralized in: - `libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts` - `libs/portal/stalker/data-access/src/lib/models/*.ts` +## Forced External Launches + +"Open in external player" needs a `create_link` round trip before it reaches +MPV/VLC. The shared rules are in +[Forced External Launches From Detail Pages](./embedded-inline-playback.md#forced-external-launches-from-detail-pages); +the Stalker keys and queues are: + +Series (`StalkerSeriesViewComponent`, `stalker-series-launch-queue.ts`): + +- Pending starts and held choices are keyed by `playlist:series` + (`currentSeriesKey`). The view is reused across series and provider ids + collide across playlists, so one series settling never drops what another + holds. +- A start is pending for its series from the click until it settles. A forced + launch stays pending through the close of the previous player, the launch + and the release of a held choice. The pending flag disables the hero button + and the menu's external-player and watched rows. +- Before launching, an episode of the same series still running externally is + closed (`replaceOwnedExternalSession`). The request is rechecked after that + close and after the launch IPC; a superseded launch closes the session it + opened. +- An episode chosen while a forced launch of its series is mid-flight is held + (`StalkerSeriesLaunchQueue.hold`); the latest choice per series wins. On + release it is dropped when the series is no longer shown. Otherwise + `replacePlayer` closes what the launch opened before the choice starts, and + an unconfirmed close drops the choice. +- An episode chosen while a watched or reset batch runs is held until the + batch settles, then goes through the usual gates only on the series it was + made for: episode identities overlap across series. + +Movies (`createStalkerVodDetailActions`, used by the catalog detail, the +collection detail and search): + +- A repeat for the same `playlist:movie` while its launch is in flight is + ignored. Launches of other movies are not held back. +- The launch joins the host's starts (`beginPendingStart`): it supersedes an + earlier start, is dropped once a later one begins, and keeps Play, Start + over, the watched toggle and the menu rows disabled until it settles. +- The resolved stream is discarded when the movie is no longer selected or a + newer start took over. The selection check includes the content type, + because movie and series ids collide. Otherwise the movie's own external + session is replaced, the inline player closes, and the launch is sent. A + launch that resolves after either condition changed closes the session it + opened; one that fails by then is not reported. +- "Reset progress" counts as a pending start of the movie until the write + lands, so a start made meanwhile cannot resume from the row being cleared. +- The pending start is owner-scoped (`createPendingPlaybackStart`). Each host + retires it when the selection leaves the owner. + +Regression coverage: `stalker-series-launch-queue.spec.ts`, +`stalker-series-view.component.spec.ts`, +`stalker-series-view.season-watch.spec.ts`, +`stalker-vod-detail-actions.spec.ts`, +`stalker-vod-playback-controller.spec.ts` and, in +`libs/portal/shared/util/src/lib/`, `pending-playback-start.spec.ts` and +`replace-owned-external-session.spec.ts`. + ## Favorites and Recently Viewed Current implementation is shared via Stalker-specific helpers: diff --git a/docs/architecture/vod-multi-source.md b/docs/architecture/vod-multi-source.md index ebd6e1cd2..7eb13709a 100644 --- a/docs/architecture/vod-multi-source.md +++ b/docs/architecture/vod-multi-source.md @@ -757,6 +757,30 @@ lookup comes back empty, because "never watched" is an answer: the button must read Play, not `Resume 42:18` on a stream that starts at zero. A pin on the route's own row changes nothing; the loaded position already IS that copy's. +## Menu launch and reset follow the primary button + +The "…" menu acts on the copy the primary button acts on. The shared launch +rules are in +[Forced External Launches From Detail Pages](./embedded-inline-playback.md#forced-external-launches-from-detail-pages). + +- "Open in external player" and "Start over" are host-owned + (`VodDetailsMenuBindings.openExternal` / `restart`); the menu service never + builds a playback itself. The route forces MPV/VLC for the pinned copy from + that copy's own resume point (`playPinnedSource` with `player` and + `replacePlaying`), also while that copy already plays: it is relaunched, + never swapped for the route copy. Only an `unavailable` pin falls through + to the route copy's Resume or Play. +- "Reset progress" clears the row of `primaryTarget`: the pinned copy's own + row, otherwise the route copy's. +- Resets in flight are a list of targets + (`VodDetailsPlaybackService.pendingResets`, `vod-details-reset-target.ts`), + not one flag: the reused page can show another movie and come back, and + resets of one copy can overlap, so each reset removes only its own entry. +- A start is refused (`startResolvedPlayback`), and Play, Start over and the + menu launch are disabled (`startBlocked`), while an external launch has not + settled or the list holds the copy the page currently acts on + (`resetTarget`). A reset still writing for another copy does not block it. + ## Provider codec metadata `info.video` / `info.audio` come back in two shapes: the declared string array diff --git a/docs/architecture/xtream-portal-compatibility.md b/docs/architecture/xtream-portal-compatibility.md index 6545321e0..c9eda0965 100644 --- a/docs/architecture/xtream-portal-compatibility.md +++ b/docs/architecture/xtream-portal-compatibility.md @@ -405,3 +405,39 @@ deduplicated list, `hasMoreContent` derives from accumulated length vs and the facade maps page 0 to the skeleton and later pages to the tail spinner. These catalog/search surfaces use incremental loading instead of page buttons. + +## Forced external launches from detail pages + +The "…" menu's MPV/VLC launch follows the shared rules in +[Forced External Launches From Detail Pages](./embedded-inline-playback.md#forced-external-launches-from-detail-pages). +The movie page's pin and reset rules are in +[VOD Multi-Source](./vod-multi-source.md#menu-launch-and-reset-follow-the-primary-button). + +The series page keeps its launch state at module level in +`serial-details-external-launch.ts`, so it outlives a recreated page: + +- The owner is `playlist:series` (`launchOwner()`). It changes when the page + shows another series and is null once the page is gone. +- Forced launches of one owner run on one chain. A later launch waits for the + earlier one to settle, closes the owner's running episode session and then + launches. Each step rechecks the owner, and a launch that resolves after the + page left the owner closes the session it opened. +- The duplicate guard is keyed by page token plus episode. The token + (`pageToken()`) is owner, page instance and visit, so a launch left behind + by an earlier visit of the same series does not swallow the reopened page's + click; that click queues on the owner's chain. +- While a forced launch of the owner is pending (`forcedLaunchPending`), a + start that does not force a player is queued instead of started. One choice + is kept per owner, the latest wins, and it carries the host and `start` of + the page that made it. Once the chain settles, the player the launch opened + is closed first while the owner stays pending. The choice is dropped when + that page no longer shows the owner or the close was not confirmed. +- The pending flag also disables the menu's external-player row and the + season and series watched actions, and counts as active playback for "Reset + progress". +- The launch-position marker and a launch-failure message apply only while + the page token is unchanged. + +Regression coverage: `serial-details-external-launch.spec.ts`, +`serial-details-playback.service.spec.ts` and +`serial-details-menu.service.spec.ts`.