fix(playback): clarify external player launch feedback (#1388)

This commit is contained in:
4gray authored and GitHub committed 2026-08-09 13:33:07 +02:00
1 parent 8442747c37
commit d73acd6bfc
101 files changed
+8819 -850

No files matched your search

+85 -2
View File
@@ -824,7 +824,90 @@ app as a real argument, so it is not an option.
exception. 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.
evidence or ownership state. MPV/VLC actions remain mounted after an attempt
and expose credential-free per-target launching/started/playing/error state;
only an exact Electron `playing` update is labelled Playing. One handshake is
allowed at a time. The renderer claims the credential-free content identity
before awaiting Electron, so primary Play is disabled and a launching or
closable-error alternative remains owned before the controller commits it.
Every route action that can start the same external playback, including
Restart and the provider-source shortcut, observes that local pre-IPC guard.
The Xtream VOD diagnostic-fallback handler records the same route-scoped
destination and pending generation before invoking MPV/VLC, so route reuse
cannot orphan that process outside the next route's close-before-play path.
Its fieldless intent is bound to the exact session returned
by the source owner's launch promise, so a late timed-out attempt cannot take
over a retry; later global updates must match that ID. A replacement waits for
confirmed teardown of the tracked external process, applies the old exact
close before launch, and cancels an unlaunched handoff if diagnostic ownership
changes. Process teardown has bounded graceful and forced confirmation
windows, and reusable MPV bounds the IPC command that precedes them; if any
stage cannot reach a confirmed exit, the exact session stays live and the
replacement fails closed instead of overlapping it. A process-wide teardown
gate starts before any potentially slow teardown preparation, including VLC
position flush and a reused player's protocol quit, and rejects every
MPV/VLC spawn until that exact child reports exit. If bounded
teardown fails while a fresh launch is still pending, that launch IPC rejects
and the exact session remains a closable error instead of hanging forever.
If a pre-content reuse failure has no still-live displaced session to restore,
the replacement error keeps its attached closer so Stop can retry the orphaned
child teardown. A terminal error without a closer is never restorable.
A failed close is single-flight only while its promise is pending: Stop can
retry the same exact child after a bounded confirmation failure. Reuse maps
the child to its current content session, so a stale older closer becomes a
no-op instead of terminating a newer `loadfile`/VLC enqueue handoff.
A duplicate close for an already closed session returns its terminal snapshot
without re-entering the saved closer, and a late process error cannot revive
that terminal session. Reused MPV commands are bound to the socket captured
for that exact child, so a later process cannot inherit a stale protocol quit.
Stop observed before a pending MPV content command or VLC enqueue command
prevents that command from dispatching. A source handoff fails closed while
a live session has no closer (`canClose: false`); renderer Dismiss is not
teardown confirmation. That denied handoff advances neither the multi-source
switch token nor the playback generation, so it cannot cancel the sole launch
already in flight.
VLC rechecks the gate at each concrete spawn after port allocation or reuse
work; if a post-start fallback is blocked there, the opened session becomes
an error rather than retaining a false started status. A failed RC-port
allocation never claims reuse ownership, so the fallback VLC child retains
its exact one-shot closer.
Reuse failures before a content command restore the globally displaced
renderer session, not the reusable process's prior owner, and only while the
exact displaced-session ID is still active; after
`loadfile`/VLC `clear` is dispatched,
the replacement owns the process and remains a closable error instead of
restoring stale content metadata. Stop during an in-flight MPV or VLC reuse
command, including during failed-command teardown or the subsequent VLC
fallback port-allocation wait, settles that exact close without falling
through to a fresh spawn;
a stopped VLC spawn error that reports only `close` also settles its original
launch IPC with the exact closed session;
a fresh fallback retires the old child's exit under its prior session so it
cannot close the replacement. Source handoffs recheck ownership after launch
and accept only `opened`/`playing`; a stale returned session is closed exactly
and a Stop-returned `closed` session is never committed. If that exact stale
close fails, its credential-free destination owner is retained for the next
close attempt. Retained destination ownership is scoped to the initiating
playlist/VOD route key, so route reuse cannot expose Stop for the previous
movie's external session. Play/Resume capture that route key before awaiting
close and cancel if navigation changes it; a late diagnostic fallback closes
its exact returned session instead of adopting it on the new route. They
supersede an older source resolution before awaiting the shared
close-before-replacement path, and accepting a diagnostic fallback retires
the same older resolution before opening MPV/VLC. They publish the route-source
badge, caption evidence, and position only after start succeeds.
Closable errors still participate in every replacement close and keep Stop as
the global dock's only teardown affordance; Dismiss is reserved for terminal
errors that have no closer. The shared `isLiveExternalPlayerSession` predicate
keeps M3U and series ownership while
such an error can still be stopped; consumers must not treat every `error`
status as terminal.
If the local handshake times out after an exact Electron session is known,
that ID remains
correlated so a later exact update can recover the UI. The global dock mirrors
those statuses, keeps closable errors visible until Stop confirms teardown and
terminal errors visible until dismissal, and intentionally has no retry because
it does not own the original launch headers or credentials.
- 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.*`
@@ -1179,7 +1262,7 @@ engine` (restart required) or
- Portal mode (full vs. simple) follows OBSERVED behavior, never a URL substring. The single predicate is `isFullStalkerPortalPlaylist()` / `isFullStalkerPortalUrl()` in `@iptvnator/shared/interfaces` (`stalker-portal-mode.util.ts`): the persisted `Playlist.isFullStalkerPortal` flag is authoritative and the URL shape is a fallback for legacy rows only. Three diverging copies of this rule used to exist and shipped broken configurations (#850/#686/#755) — never re-implement it. A token-enforcing `portal.php` panel is a full portal; a `server/load.php` endpoint that answers without a token is a simple one.
- Import probes candidates in order (a pasted `.php` endpoint first, then `<base>/portal.php` → `<base>/server/load.php` → `<base>/stalker_portal/server/load.php`) and classifies each by behavior — a token-less `itv/get_genres` returning data proves a token-free panel; the plain-text auth failure proves a full portal, confirmed by a real handshake + `get_profile`. `StalkerPortalDiscoveryService` (`libs/portal/stalker/data-access`) persists the proven endpoint and mode.
- `executeStalkerRequest()` (`stores/utils/stalker-request.utils.ts`) is the choke point for catalog, content and playback requests: mode routing, the in-session repair override, and retry-once all live there. Four callers are deliberately outside it because they run below or before the thing it routes on — `StalkerAuthApi` (handshake/`get_profile`/`do_auth`, which the full-portal branch is built from; routing them back would recurse), `StalkerPortalDiscoveryService` (probes precede the mode they determine), `StalkerAccountInfoService.fetchViaProfile()`, and `StreamResolverService` for a collection item with no playlist row. They are exempt from the routing, not from the repair it hooks, but only `fetchViaProfile()` wires `StalkerPortalRepairService` itself: discovery is what repair *drives*, the row-less resolver branch has no playlist to repair, and the auth layer needs nothing — a terminal handshake failure propagates out of the full-portal branch into whichever `executeStalkerRequest()` call triggered the authentication, which is why terminal handshake failures are a repair trigger. Anything new that is not auth or discovery belongs on `executeStalkerRequest()`. Existing playlists are repaired LAZILY (`StalkerPortalRepairService`) — only after a request fails with a shape a wrong endpoint/mode produces, at most once per source configuration per playlist per session, persisted through the atomic `PlaylistsService.transformPlaylistMeta`. There is deliberately **no eager one-shot migration**: a portal that works is never re-probed.
- `executeStalkerRequest()` (`stores/utils/stalker-request.utils.ts`) is the choke point for catalog, content and playback requests: mode routing, the in-session repair override, and retry-once all live there. Four callers are deliberately outside it because they run below or before the thing it routes on — `StalkerAuthApi` (handshake/`get_profile`/`do_auth`, which the full-portal branch is built from; routing them back would recurse), `StalkerPortalDiscoveryService` (probes precede the mode they determine), `StalkerAccountInfoService.fetchViaProfile()`, and `StreamResolverService` for a collection item with no playlist row. They are exempt from the routing, not from the repair it hooks, but only `fetchViaProfile()` wires `StalkerPortalRepairService` itself: discovery is what repair _drives_, the row-less resolver branch has no playlist to repair, and the auth layer needs nothing — a terminal handshake failure propagates out of the full-portal branch into whichever `executeStalkerRequest()` call triggered the authentication, which is why terminal handshake failures are a repair trigger. Anything new that is not auth or discovery belongs on `executeStalkerRequest()`. Existing playlists are repaired LAZILY (`StalkerPortalRepairService`) — only after a request fails with a shape a wrong endpoint/mode produces, at most once per source configuration per playlist per session, persisted through the atomic `PlaylistsService.transformPlaylistMeta`. There is deliberately **no eager one-shot migration**: a portal that works is never re-probed.
- Both transports build the wire format from the same shared builders in `@iptvnator/shared/interfaces` — `buildStalkerRequestUrl()`, `buildStalkerIdentityRequestContext()`, `encodeStalkerCmdValue()` — so the Electron and PWA legs cannot drift. The mock's `/stalker` mirror shares the identity builder only — it dispatches in-process, so there is no portal URL to build and it mirrors the `JsHttpRequest` default by hand. Never fork any of them.
- Simple portals skip the auth lifecycle (no handshake, token or watchdog) but their requests are not stripped to a bare cookie: they still carry everything the shared builder derives from a MAC alone (`mac`/`stb_lang`/`timezone` cookie, MAG `User-Agent`/`X-User-Agent`, `Accept` set). They do NOT carry the serial — `dispatchStalkerRequest()`'s direct branch forwards only `url`/`macAddress`/`params`, so no `SN` header and no serial-derived `__cfduid`, whatever the playlist stores. That gate is on API requests only: `buildStalkerExternalPlaybackHeaders()` reads the serial off the playlist row with no mode check, so the same simple-mode playlist does send `SN`/`__cfduid` with a portal-owned stream.
- Contract: `docs/architecture/stalker-portal.md` ("Portal Mode and Endpoint Discovery", "Request Transport and `cmd` Encoding").