* feat(ui): slim ContentHero to non-scrolling hero block The page scroll container moves out of the hero into the upcoming PortalDetailShellComponent; the hero no longer hosts a default content slot for player/seasons/extras. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): add PortalDetailShellComponent with browse/watch states Two-state detail layout shell: owns page scroll, hero collapse animation (~300ms, reduced-motion aware), Escape-to-close-player handling, and an About block that re-stamps host-provided *detailTags/*detailMeta templates in watch state. The [detail-player] slot is never wrapped in a shell conditional so the host-owned player subtree survives state changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(playback): restyle inline player header as now-playing bar Adds a back button (emits the existing closed output — in watch state back returns to browse, not route navigation) and replaces the hardcoded header strings with i18n keys (PORTALS.NOW_PLAYING, CLOSE_PLAYER, BACK_TO_BROWSE). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(xtream): migrate VOD and serial detail templates to detail shell Hero chips/meta/actions become *detailTags/*detailMeta/*detailActions templates; the inline player moves to the full-width [detail-player] slot; trailer and similar rail move to [detail-extras]. Escape and the now-playing back button close the inline player back to browse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(stalker): migrate shared VodDetails and series view to detail shell The shared VodDetailsComponent (Stalker VOD, collections, search) and StalkerSeriesViewComponent move their hero content into *detailTags/*detailMeta/*detailActions templates and host the inline player in the full-width [detail-player] slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): season tabs replace two-level seasons navigation SeasonTabsComponent renders a pill row (dropdown beyond 6 seasons), an optional season description and a back-to-playing chip. The season container auto-selects a season (inline-playing episode's season → most recent in-progress → first), emits seasonSelected for auto-selections so lazy-load/enrichment hooks keep firing, highlights the inline-playing episode, and loses the seasons grid + 'Back to seasons' button. Download/progress helpers move to pure utils; season strings are now translatable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(portals): season descriptions under the season tabs Xtream reads season overviews from get_series_info; Stalker exposes the TMDB season overview through TmdbEnrichmentService.getSeason (same cache rows as the episode enrichment) and keys it per tmdbId so views reused across detail navigations cannot leak descriptions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: document two-state detail layout and season tabs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): app-prefix detail template directive selectors, build fixes detailTags/detailMeta/detailActions → appDetailTags/appDetailMeta/ appDetailActions per @angular-eslint/directive-selector; type the Escape host listener as Event; drop a redundant ?? in season tabs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(i18n): translate new detail-page keys across all 17 locales 13 PORTALS.* keys (now-playing bar, About block, season tabs, episode empty states) translated via the i18n-fill workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): keep now-playing bar actions inside the viewport The nowrap title made the header grid track grow to min-content and pushed Copy/Close out of the page; min-width: 0 on the header/title row lets the title truncate instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): expandable season description and episode info dialog Season descriptions get the same show more/less toggle as the hero (new shared ExpandableTextComponent); episode cards and list rows get an info button that opens a dialog with the full plot, duration, air date and a Play action — card heights stay fixed and click-to-play semantics stay intact. Also: drop the per-frame backdrop blur on the player card and make the watch-enter scroll instant so the hero morph stops competing for frame budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): re-run TMDB season enrichment when the show match arrives With season tabs the first seasonSelected fires as soon as seasons load — usually before the async show-level TMDB enrichment has written tmdb_id — so the season fetch silently no-oped and was never retried, leaving episode stills/plots and the season description empty. Both detail hosts now key the fetch on (tmdb_id, selected season) in an effect, so it runs whichever arrives last. Also adds breathing room above the About divider in watch state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): move episode info button into the card body, compact overlay actions Three overlay buttons crowded the thumbnail on hover and the new info button had no styling at all. Info belongs with the content text: it now sits as a quiet ghost button at the end of the title row (revealed on card hover, MDC touch target clamped so it cannot swallow the play-on-click card area). Download + watched-toggle stay on the artwork, shrunk to 28px with a subtle edge border. List view already used the shared action style and is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): single watched indicator-toggle on episode cards Watched state was signalled three times on one card: the green badge, the hover toggle in the top-right cluster, and a checkmark appended to the title. Now one control does both jobs — a toggle at the top-left of the artwork that looks like the old green badge when watched (always visible) and appears as a ghost circle on hover when not. The right cluster keeps only the download action; the title checkmark is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(playback): back arrow in watch mode navigates straight to the list Leaving the theater took two clicks: back to browse, then back to the list. Watch state already shows everything browse offers (episodes, About, extras), so the bar's back arrow is now route-level back (new backClicked output wired to each host's goBack), while Close player and Escape keep exiting to browse without navigating. Replaces PORTALS.BACK_TO_BROWSE with a generic top-level BACK key (all locales). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): compact episode stills in the list view When a season has genuinely distinct per-episode art (TMDB stills or provider images), list rows show a 96x54 thumbnail with the episode number riding on it instead of the number square. When every episode repeats the same image (providers often send the series poster), the plain number square stays — a column of identical posters is worse than none. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(ui): compact list-thumbnail spec under the max-lines limit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): address Greptile review — real fallback asset, untracked overflow measure default-episode.png never existed (pre-existing in the grid card, copied into the list thumbs) — both onerror handlers now fall back to the real default-poster.png. The expandable-text and content-hero measuring effects read the expanded signal via untracked so toggling show more/less no longer rebuilds their ResizeObservers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
22 KiB
Embedded Inline Playback
This document records the current contract for embedded playback in portal detail views.
Summary
- Embedded web players are
videojs,html5, andartplayer. embedded-mpvexists as a hidden desktop experimental harness backed by a native MPV addon. macOS and Windows use in-processlibmpv; Linux uses an X11/Xwayland child window with an out-of-processmpv --widbackend.- Controlled external players are
mpvandvlc. - macOS
.appbundle paths are resolved only for real MPV/VLC apps. IINA may launch through the MPV path field when the user supplies an executable path such as/Applications/IINA.app/Contents/MacOS/iina-cli, but IPTVnator controls, position polling, and instance reuse are not guaranteed for IINA. - Flatpak launches external players on the host via
flatpak-spawn --host. - Live playback stays inline in dedicated live layouts.
- VOD and series detail playback stays inline on canonical detail, collection, favorites, recent, and search surfaces.
- Xtream and Stalker series detail heroes expose a quick-start CTA driven by saved episode playback positions.
- Embedded playback UI is always hosted by the current view.
PlayerServicelaunches 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.
Scope
Inline embedded playback is required for these VOD/series entry points:
- Xtream VOD detail route
- Xtream series detail route
- Stalker VOD detail view
- Stalker series detail view
- unified favorites collection details
- unified recently viewed collection details
- Stalker advanced search result details
Collection/search VOD surfaces that expose embedded playback must host
ResolvedPortalPlayback inline state locally. They must not call
PlayerService.openPlayer(...) or PlayerService.openResolvedPlayback(...) to
create embedded UI.
Embedded MPV Harness
The repository now contains a first-pass native embedded MPV harness for Electron:
- shared setting id:
embedded-mpv - native addon owner:
/Users/4gray/Code/iptvnator/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts - IPC bridge:
/Users/4gray/Code/iptvnator/apps/electron-backend/src/app/events/embedded-mpv.events.ts - renderer host:
/Users/4gray/Code/iptvnator/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts - native architecture and release-readiness details:
/Users/4gray/Code/iptvnator/docs/architecture/embedded-mpv-native.md
Current contract:
- desktop only: macOS, Windows x64, and Linux x64 under X11/Xwayland
- experimental opt-in
- enabled in local development only when
IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT=1 - Linux Wayland sessions must start Electron through Xwayland, for example with
pnpm nx run electron-backend:serve-electron --args=--ozone-platform=x11during local development oriptvnator --ozone-platform=x11for a packaged app - enabled in packaged desktop builds only when the bundled native addon/runtime prerequisites are present; Linux additionally requires an
mpvexecutable onPATH - uses IPTVnator-owned controls and
ResolvedPortalPlaybackpayloads - uses the libmpv render API on macOS and renders through an IPTVnator-owned native
NSView - uses mpv
widembedding on Windows and Linux through IPTVnator-owned native child windows - Linux starts
mpv --wid=<x11-window>out of process so MPV does not share Electron's FFmpeg or graphics symbols - Linux controls that out-of-process MPV instance through a private JSON IPC socket so duration, position, pause, volume, and seek state come from MPV instead of renderer guesses
- Linux starts that MPV process with
WAYLAND_DISPLAYremoved,XDG_SESSION_TYPE=x11, and X11 video output options; otherwise MPV can pick Wayland inside a Wayland desktop session, ignore--wid, and open a separate window instead of embedding - macOS/Windows default to libmpv's OpenGL render backend with
hwdec=auto-safe - keeps the previous software renderer as a debug fallback via
IPTVNATOR_EMBEDDED_MPV_RENDERER=sw - emits lightweight native diagnostics when
IPTVNATOR_TRACE_EMBEDDED_MPV=1is set; Linux also writes MPV's own trace log to/tmp/iptvnator-embedded-mpv.log - exposes an IPTVnator-owned fullscreen button that uses the renderer fullscreen API and resyncs the native MPV view bounds after fullscreen transitions
- auto-hides IPTVnator-owned controls while playback is active and restores them on pointer/focus interaction
- exposes audio-track metadata from MPV and switches tracks through the
aidproperty without reloading the stream - passes VOD/episode resume offsets to MPV through the
loadfileoptions map; live catchup URLs are treated as already-positioned streams - applies the initial volume during session creation and uses async libmpv control calls after startup where the in-process libmpv backend is active
- VLC remains external-only
Current limitation:
- the current feasibility harness is still experimental and platform-specific
- the original macOS
widembedding path produced audio with a black video surface inside Electron, so the harness now avoids foreign-window embedding on macOS - Windows and Linux use the mpv
widpath and still need OS-native packaged smoke coverage before public exposure - Linux native Wayland is not supported in this implementation; the Electron process must have
DISPLAYthrough X11/Xwayland and a real X11 window handle - the OpenGL render path avoids the old per-frame
CGImagecopy path, but it still needs broader interaction, resize, and packaging coverage - startup deadlocks seen during early macOS playback bring-up are mitigated, but the feature is still kept behind the explicit experiment flag until more interaction and packaging coverage is proven
- because of that, the setting is auto-sanitized back to the default inline player unless support detection reports that the experimental runtime is available
- this follows the rollout gate: keep the native work in-tree, but do not leave it user-facing until playback, resize, focus, and packaging are stable
Two-State Detail Layout (Browse ↔ Watch)
Portal VOD/series detail pages are hosted by the shared
PortalDetailShellComponent
(libs/ui/components/src/lib/portal-detail-shell/portal-detail-shell.component.ts),
which owns the page scroll container and two layout states:
- Browse (
playbackActive=false): the hero (ContentHeroComponent, now a natural-height, non-scrolling block) renders poster, title, chips, description, credits, and actions. Episodes render below. - Watch (
playbackActive=true, bound by hosts toinlinePlayback() !== null): the hero collapses (~300ms CSS morph, disabled underprefers-reduced-motion), the host-projected[detail-player]slot renders the inline player at full content width, and the shell renders an About block (ContentAboutComponent) below the[detail-episodes]slot so the hero metadata stays reachable.
Contracts:
- Hosts provide hero chips/meta/actions as
*appDetailTags/*appDetailMeta/*appDetailActionstemplates; the shell stamps them into the hero and again into the About block. Degradation stays "missing → not rendered". - The shell never wraps the
[detail-player]slot in a conditional; the host's@if (inlinePlayback())is the only creator/destroyer of the player subtree, so shell state changes cannot recreate the<video>. - External MPV/VLC sessions do not flip the layout to watch — browse layout stays, and the primary CTA keeps its "Stop " behavior.
- Escape closes inline playback: the shell emits
closePlayerRequestedwhen playback is active, the event was notdefaultPrevented, and no element is in browser fullscreen; hosts wire it tocloseInlinePlayer(). - The now-playing bar separates two exits: the back arrow emits
backClicked, which hosts wire to their route-levelgoBack()(straight back to the list — everything browse offers is also visible in watch, so a two-step unwind would be ceremony); the "Close player" button and Escape emitclosed/closePlayerRequestedand return to browse without navigating. - Entering watch scrolls the shell to the top; leaving keeps the scroll position.
Season navigation inside SeasonContainerComponent uses season tabs
(SeasonTabsComponent; a dropdown beyond 6 seasons) instead of the old
seasons-grid + "Back to seasons" level. A season is auto-selected
(inline-playing episode's season → most recently updated in-progress
episode's season → first) and the auto-selection emits seasonSelected,
so host lazy-load/enrichment hooks (Stalker VOD-series episode fetch,
TMDB season fetch, Xtream enrichSelectedSerialSeason) fire on open
without a click. Switching tabs never stops playback; a "back to playing
episode" chip appears when the playing episode is outside the opened
season. Season descriptions come from get_series_info seasons (Xtream)
or TmdbEnrichmentService.getSeason (Stalker).
Components
Shared detail layout shell:
/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/portal-detail-shell/portal-detail-shell.component.ts
Shared inline player shell:
/Users/4gray/Code/iptvnator/libs/ui/playback/src/lib/portal-inline-player/portal-inline-player.component.ts
Xtream detail hosts:
/Users/4gray/Code/iptvnator/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts/Users/4gray/Code/iptvnator/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts
Stalker detail hosts:
/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts
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:
/Users/4gray/Code/iptvnator/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.ts/Users/4gray/Code/iptvnator/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts
Playback Decision Rule
When a detail view starts playback:
- Resolve or construct a typed playback payload.
- Check the active player setting.
- If the player is embedded, render the inline player inside the current detail view.
- If the player is external, hand the same payload to
PlayerServicefor MPV/VLC playback.
The detail or collection/search host owns inline state. PlayerService is not
an owner of embedded UI playback state.
Series Quick Start CTA
Xtream and Stalker series detail views share the quick-start decision helper in
libs/portal/shared/util/src/lib/series-quick-start.ts.
The helper flattens the loaded season/episode map, sorts seasons and episodes in
natural order, and returns the hero CTA state.
Current contract:
- the CTA shows the action label plus a compact episode target such as
S01E02 · Episode title - if an episode is in progress, resume the latest updated in-progress episode with its saved offset
- if no episode is in progress, play the first unwatched episode in season order
- if watched episodes end at a season boundary, play the first episode of the next loaded season
- if every loaded episode is watched, render a disabled completed state
The click path must continue through each detail host's normal episode playback method so recent-item updates, inline/external player selection, resume offsets, and playback-position saving keep the same behavior as manual episode clicks.
Series Inline Episode Continuation
Xtream and Stalker series detail views own current-episode state and pass a
SeriesPlaybackNavigation payload through PortalInlinePlayerComponent and
WebPlayerViewComponent to the active embedded player.
Current contract:
- Video.js, HTML5, ArtPlayer, and embedded MPV emit
playbackEndedonly for real media EOF/endedevents. - Teardown, replacement, manual close, player reload, idle state, and playback
errors must not emit
playbackEnded. - Series previous/next controls render only when the series navigation payload is present. Movies, live streams, radio streams, and non-series VOD must not show those buttons.
- The shared navigation payload contains
canPrevious,canNext, andautoplayEnabled. Player controls disable previous on the first episode and next on the last episode of the current season. - Autoplay is enabled by default for series playback. On
playbackEnded, the portal detail host starts the next episode only whencanNextis true for the current season. - Autoplay and Next never cross season boundaries or lazy-load another season. Quick start remains the flow that may choose an episode from another loaded or newly loaded season before playback begins.
- Previous switches directly to the previous episode in the current season. It does not implement a restart-threshold behavior.
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 diagnostics remain client-only:
- no ffprobe or server-side probing
- no extra manifest fetch beyond the active player
- no automatic failover to an external player
- no embedded MPV diagnostics
Supported diagnostic codes are:
unsupported-containerunsupported-codecmedia-decode-errornetwork-errorbrowser-access-errordrm-or-encryptionunknown-playback-error
network-error is reserved for provider/network loading failures. Browser security failures such as CORS, mixed content, Content Security Policy, and private-network-access blocks are classified as browser-access-error so the UI can explain that the browser player was blocked before playback reached decoding.
mpegts.js Early-EOF failures on MPEG-TS streams are classified as media-decode-error instead of generic network-error. These failures usually mean the fetch stream ended before mpegts.js expected a complete transport stream, and external players may still handle the same URL more tolerant of short reads or malformed TS boundaries.
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, and raw HLS/mpegts details. 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.
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.
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.
External Player Arguments
Electron settings expose optional MPV and VLC command-line argument fields only
when the corresponding external player is selected. The executable path remains a
path-only setting; extra flags are stored separately as mpvPlayerArguments and
vlcPlayerArguments.
Argument fields are line-oriented: one non-empty trimmed line becomes one argv entry. IPTVnator prepends those custom entries before its stream-specific runtime arguments, then keeps the stream URL last. This avoids shell parsing, keeps paths with spaces safe, and preserves existing settings for users who never configured extra arguments.
The arguments apply only when IPTVnator spawns a new external player process. If MPV or VLC instance reuse is active and an existing process is reused, subsequent streams are loaded through MPV IPC or VLC RC commands and new process arguments are not re-applied until a fresh process starts.
Electron External Player Ownership
External MPV/VLC integration is split across focused main-process modules:
apps/electron-backend/src/app/events/player.events.tsregisters IPC handlers and settings updates only.apps/electron-backend/src/app/events/external-player-launch-context.tsresolves Flatpak spawning, default executable paths, macOS.appbundles, custom argv merging, spawn specs, and reuse decisions.apps/electron-backend/src/app/events/external-player-playback-request.tsbuilds the effective playback request, including Stalker direct-stream fallback metadata and external-player request headers.apps/electron-backend/src/app/events/external-player-runtime.tsowns shared session tracking, trace logging, renderer notifications, playback-position forwarding, and user-facing start errors.apps/electron-backend/src/app/events/mpv-session.service.tsowns MPV process, socket, reuse, cleanup, and progress polling lifecycle.apps/electron-backend/src/app/events/vlc-session.service.tsowns VLC process, RC interface, reuse, cleanup, command parsing, and progress polling lifecycle.
Keep player-specific process state in the MPV/VLC session modules. Shared spawn,
request-header, session-registry, and notification helpers belong in the
external-player-* modules so IPC registration stays small and reviewable.
Flatpak External Players
Flatpak cannot execute host-installed mpv or vlc binaries directly from the sandbox.
Current contract:
- Flatpak launches external players through
flatpak-spawn --host. - AppImage, deb/rpm, snap, macOS, and Windows keep the existing direct process spawn flow.
- VLC keeps the current external-session flow in Flatpak, including the RC port used for progress polling.
- MPV is intentionally reduced in Flatpak: the app does not reuse an existing MPV instance there and does not open the Unix socket bridge used for non-Flatpak progress polling.
- VLC instance reuse is also gated off in Flatpak. Outside Flatpak the user can opt in via the "Reuse VLC instance" setting; the app then keeps a single tracked VLC process and drives subsequent stream loads through its RC interface (
clear+add <url> :http-*) instead of spawning a new window per click.
This keeps non-Flatpak behavior unchanged while allowing Flatpak builds to open host-installed external players.
Typed Playback Payload
Shared playback payloads live in:
/Users/4gray/Code/iptvnator/libs/shared/interfaces/src/lib/portal-playback.interface.ts
Types introduced:
PlayerContentInfoResolvedPortalPlayback
These provide a single shape for:
streamUrltitle- optional thumbnail and resume start time
- playback-position metadata
- optional external-player headers and request metadata
Xtream Behavior
Xtream detail views already own canonical routes, so they construct playback locally and decide inline vs external locally.
Behavior to preserve:
- resume/playback position continues saving from
timeUpdate - back navigation clears inline playback with the route
- favorites, recent, and search still route into canonical Xtream detail screens before playback
Stalker Behavior
Stalker previously resolved playback and opened UI in the same method.
Current contract:
resolveVodPlayback(...)returns aResolvedPortalPlaybackcreateLinkToPlayVod(...)remains as a compatibility API but collection, search, and canonical detail views use the resolver directly- Stalker detail, collection, and search views decide inline vs external locally
This keeps:
- inline/store-state detail navigation intact
- series and VOD-as-series support intact
- external MPV/VLC launches unchanged
Playback Position Saving
The old dialog path saved playback positions from inside the removed Xtream player dialog.
The new contract is:
- inline detail hosts listen to
timeUpdate - each host throttles saves
- each host persists via existing playback-position infrastructure
This avoids coupling inline UI state to a global dialog.
Future Migration Rule
If a non-detail surface needs embedded playback:
- give that surface a canonical inline host
- switch it to
ResolvedPortalPlayback - do not move portal-specific navigation into
PlayerService
The preferred direction is view-owned inline playback, not a larger dialog manager.