mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
docs(portals): document forced MPV/VLC launch and pending-start contracts (#1809)
* 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> * docs(portals): correct launch-contract claims against the code - A launching session is published before the launch IPC resolves; what it lacks until then is an exact closer. - The series watched/reset batch and the Xtream movie launch gate are page-wide, not owner-scoped. - Only the Stalker movie hosts retire a pending start, and that does not clear the repeat guard of a launch still in flight. - Name only the specs that exercise the Xtream series rules. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(portals): tighten closer timing and page-wide gate wording A launching session may already have its closer before the launch IPC resolves, the Xtream movie launch gate does not cover the inline player's diagnostic fallback, and the hero-state spec covers only the external-player and reset rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(portals): fix stale session-timing comment and search guard wording - serial-details-external-launch.ts: Electron publishes a `launching` session as soon as the launch IPC arrives, not only after the launch settles. Comment only; no behaviour change. - stalker-portal.md: the search host's selection check does not include the content type; a switch to a series supersedes the launch through the playback owner key instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
5 files changed
+183
-4
No files matched your search
@@ -471,6 +471,58 @@ position telemetry overwrites this launch marker when available. This keeps the
|
|||||||
last-watched season and episode correct even when an external player's progress
|
last-watched season and episode correct even when an external player's progress
|
||||||
interface is unavailable; exact external timestamps remain best-effort.
|
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 until it
|
||||||
|
resolves the session is at most `launching` and may not have a closer yet.
|
||||||
|
Every detail host therefore keeps these rules:
|
||||||
|
|
||||||
|
- **One external player per owner.** Before launching, the host closes the
|
||||||
|
external session the page owns: the session of the same title on the
|
||||||
|
Stalker pages and the Xtream series page; the session it launched, else the
|
||||||
|
one matching its movie, on the Xtream movie page. Sessions the page does not
|
||||||
|
own are 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
|
||||||
|
`closeRunningExternalSession` with the same outcome rules.
|
||||||
|
- **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 starts are owner-scoped.** A start still resolving 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. The Stalker movie hosts, whose starts wait on a portal round trip,
|
||||||
|
also `retire(owner)` when the selection leaves it, so a start that never
|
||||||
|
settles does not keep the flag set on a return to the same title. A movie's
|
||||||
|
"Reset progress" is scoped the same way.
|
||||||
|
- **Two gates are page-wide.** The Xtream movie page refuses Play, Start
|
||||||
|
over, source switches and the menu launch while an external launch it made
|
||||||
|
has not settled. A series page runs one watched or reset batch at a time,
|
||||||
|
whichever series is shown; the Stalker page also holds episode starts until
|
||||||
|
that batch settles.
|
||||||
|
|
||||||
|
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
|
## Series Quick Start CTA
|
||||||
|
|
||||||
Xtream and Stalker series detail views share the quick-start decision helper in
|
Xtream and Stalker series detail views share the quick-start decision helper in
|
||||||
|
|||||||
@@ -1559,6 +1559,69 @@ 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/stalker-vod.utils.ts`
|
||||||
- `libs/portal/stalker/data-access/src/lib/models/*.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 the launch queue's 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 in one slot
|
||||||
|
tagged with its series; the last choice wins. When the batch settles it
|
||||||
|
goes through the usual gates only if that series is still shown: 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, also after leaving the movie and returning to it. 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. Movie and series ids collide, so the catalog and
|
||||||
|
collection details include the content type in the selection check; in
|
||||||
|
search, a switch to a series changes the playback owner instead, which
|
||||||
|
supersedes the launch. Otherwise the movie's own external
|
||||||
|
session is replaced, the host's `beforeExternalLaunch` hook runs (the
|
||||||
|
catalog and collection details close their inline player there), 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; that clears the pending
|
||||||
|
flag, not the repeat guard of a launch still in flight.
|
||||||
|
|
||||||
|
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
|
## Favorites and Recently Viewed
|
||||||
|
|
||||||
Current implementation is shared via Stalker-specific helpers:
|
Current implementation is shared via Stalker-specific helpers:
|
||||||
|
|||||||
@@ -757,6 +757,32 @@ 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
|
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.
|
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`) while a launch this page made
|
||||||
|
has not settled or the list holds the copy the page currently acts on
|
||||||
|
(`resetTarget`). `startBlocked` disables Play, Start over and the menu
|
||||||
|
launch on those conditions and while a matched session is still
|
||||||
|
`launching`; the menu rows are also held while a start is pending. A reset
|
||||||
|
still writing for another copy does not block it.
|
||||||
|
|
||||||
## Provider codec metadata
|
## Provider codec metadata
|
||||||
|
|
||||||
`info.video` / `info.audio` come back in two shapes: the declared string array
|
`info.video` / `info.audio` come back in two shapes: the declared string array
|
||||||
|
|||||||
@@ -405,3 +405,40 @@ deduplicated list, `hasMoreContent` derives from accumulated length vs
|
|||||||
and the facade maps page 0 to the skeleton and later pages to the tail
|
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
|
spinner. These catalog/search surfaces use incremental loading instead of
|
||||||
page buttons.
|
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 a launch from the
|
||||||
|
reopened page; that launch 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` (chain,
|
||||||
|
duplicate guard, queued choice), `serial-details-playback.service.spec.ts`
|
||||||
|
(page token) and, for the external-player and reset rows,
|
||||||
|
`libs/ui/components/src/lib/detail-ui/series-hero.state.spec.ts`.
|
||||||
@@ -198,10 +198,11 @@ function closeOwnedEpisodeSession(
|
|||||||
* still running externally is closed first: with instance reuse off a
|
* still running externally is closed first: with instance reuse off a
|
||||||
* second detached player would start beside it. When that close fails, or
|
* second detached player would start beside it. When that close fails, or
|
||||||
* the user moved on while it ran, the running player stays and nothing new
|
* the user moved on while it ran, the running player stays and nothing new
|
||||||
* launches. A repeat of the same episode before its launch settled
|
* launches. A repeat of the same episode before its launch settled is
|
||||||
* (Electron publishes the session only afterwards) is ignored; another
|
* ignored (the session cannot stand in for this guard: Electron publishes
|
||||||
* episode of the series waits for that launch to settle and then replaces
|
* it only once the launch IPC arrives, as `launching` until that resolves);
|
||||||
* it like any later start.
|
* another episode of the series waits for that launch to settle and then
|
||||||
|
* replaces it like any later start.
|
||||||
*/
|
*/
|
||||||
export async function openEpisodeExternally(
|
export async function openEpisodeExternally(
|
||||||
host: SeriesExternalLaunchHost,
|
host: SeriesExternalLaunchHost,
|
||||||
|
|||||||
Reference in new issue
Block a user