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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-04 09:19:32 +02:00
1 parent e8b181fcea
commit b87c88833b
4 files changed
+162

No files matched your search

@@ -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
+57
View File
@@ -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:
+24
View File
@@ -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
@@ -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`.