# Embedded Inline Playback This document records the current contract for embedded playback in portal detail views. ## Summary - Embedded web players are `videojs`, `html5`, and `artplayer`. - `embedded-mpv` exists as a hidden desktop experimental harness backed by a native MPV addon. macOS and Windows use in-process `libmpv`; Linux uses an X11/Xwayland child window with an out-of-process `mpv --wid` backend. - Controlled external players are `mpv` and `vlc`. - macOS `.app` bundle 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. `PlayerService` launches MPV/VLC only and does not open an embedded-player dialog. - Browser-player failures are diagnosed client-side and produce ranked, user-triggered recovery 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. ## Logical Playback Identity Every inline playback host owns a required, URL-independent `playbackSessionKey`. Live hosts derive it from the playlist/source and the current channel identity; an M3U session uses `Channel.id` rather than a mutable stream or catch-up URL. VOD and series hosts use the route or catalog content identity, with series episode coordinates. Stalker episode identity also includes the explicit series mode, normalized parent, exact season key, season number, and episode number; synthesized episode hashes are not identity. Mapped episodes retain the original provider command or episode ID for playback resolution. Recovery ownership never serializes that value into the session key or retained recovery state; its recovery-ownership use is limited to a short-lived exact pending-request snapshot/guard that locates the selected episode and rejects stale or out-of-order completion. This keeps token refreshes in one logical session while ensuring colliding tracking hashes cannot select a sibling episode. After a Stalker episode mounts, the host retains only a frozen structural identity (source, normalized parent, series mode, exact season key, season and episode coordinates, and the credential-free session key). Each metadata, navigation, or autoplay read resolves those coordinates against the current `mappedSeasons()` value. Same-owner provider or TMDB refreshes therefore update the mounted episode without changing its session key; if the episode disappears, or if multiple episodes claim the same structural coordinates, the mounted commands fail closed. The command/ID-bearing identity never outlives the pending playback request. Shared wrappers (`VodDetailsComponent` and `PortalInlinePlayerComponent`) pass the key unchanged to `WebPlayerViewComponent`. Hosts invalidate both committed playback and pending resolution when the canonical owner changes (playlist/source, content, and mode where applicable). Refreshing data for the same canonical owner preserves the mounted player. Collection UIDs remain a separate persistence concern; legacy M3U collection UIDs continue to use stream URLs so saved favorites ordering remains compatible. Temporary portal URLs, catch-up URLs, headers, DRM data, and alternative source payloads are transport details and must not change this logical identity. A content or episode change must produce a different key. The serialized key is created with `createPlaybackSessionKey()` from `@iptvnator/playback/util` so delimiter-bearing provider IDs remain unambiguous. The key is host-owned and stable only for the logical selection represented by that mounted host. In particular, M3U identity uses the current playlist/source identity and `Channel.id`; it does not claim durability across a refresh that replaces either identity. Recovery state contains this credential-free key, target IDs, generation counters, and a finite VOD resume position. It never uses a playback URL, headers, DRM configuration, or credentials as identity. Inline hosts capture this identity before asynchronous playback resolution. A completion may mount only while the same owner is current; stale completions and embedded starts without a complete canonical identity are ignored without replacing an already committed session. ## 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=x11` during local development or `iptvnator --ozone-platform=x11` for a packaged app - enabled in packaged desktop builds only when the bundled native addon/runtime prerequisites are present; Linux additionally requires an `mpv` executable on `PATH` - uses IPTVnator-owned controls and `ResolvedPortalPlayback` payloads - uses the libmpv render API on macOS and renders through an IPTVnator-owned native `NSView` - uses mpv `wid` embedding on Windows and Linux through IPTVnator-owned native child windows - Linux starts `mpv --wid=` 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_DISPLAY` removed, `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=1` is 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 `aid` property without reloading the stream - passes VOD/episode resume offsets to MPV through the `loadfile` options 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 `wid` embedding 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 `wid` path 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 `DISPLAY` through X11/Xwayland and a real X11 window handle - the OpenGL render path avoids the old per-frame `CGImage` copy 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 to `inlinePlayback() !== null`): the hero collapses (~300ms CSS morph, disabled under `prefers-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` / `*appDetailActions` templates; 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 `