The downloaded movie's provider action stays enabled while a start was
pending, so a second press could start beside a launch already inside
the player IPC. The series menu's reset row stayed enabled while a
watched batch persisted, although the hosts refuse the reset meanwhile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- A forced Xtream episode launch returns its promise to the menu, which
reports a rejection; the bookkeeping consumes a settled copy.
- Launch bookkeeping is guarded by a page token (series + visit), checked
again after the asynchronous save, so neither a return to the same
series nor a navigation during the save contaminates another page.
- A Stalker launch re-anchors to the host's request id after the host's
own inline teardown: only a later start supersedes it, and that still
silences the old launch's failure.
- The shared movie page disables Play and Start Over while a start is
pending, so nothing starts beside an in-flight launch.
- Episode sublines honour duration_secs before the formatted duration.
- The external session → episode ids derivation moves into the launch
helper.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- A queued Xtream episode launch checks the page owner before it closes
the running player, so a viewer who left the series keeps playback.
- A Play or episode choice made while a forced launch settles starts
once it settled instead of being dropped; only the latest choice runs.
- Both series menus disable the bulk watched actions while a start has
not published its session yet.
- The series-menu E2E helpers are shared, which brings the Stalker web
spec back under the line limit; the inline episode state derivation
moves into ui/playback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Collection details reuse the mounted view across playlists whose movies
share a numeric id; each playlist's movie now gets its own decision.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The hero keys its one-time stage/compact decision on a stable content
identity (provider + id) that every host passes, so a TMDB rename plus
a late backdrop no longer flips the visible hero.
- An explicit Stalker movie launch joins its host's start generation: it
supersedes an earlier start and is dropped once a Play, Start Over or
later launch begins, on the controller, search, collection and catalog
hosts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A second "Open in external player" while the first create_link is still
resolving is ignored, and the menu row is disabled for that window, so
two resolutions cannot each start a player.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The external player rows of every details menu follow the managed
external player capability instead of the downloads availability.
- An explicit Stalker movie launch is a pending start for the selected
movie while its stream resolves, so the reset row is disabled meanwhile.
- The resume clock formatter moves into the presentation helpers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The shared movie menu offers the external player in provider-only
mode too; that mode hides local/download controls, not the provider's
stream in MPV/VLC.
- The movie and Stalker series reset-progress rows are also disabled
while a start is still resolving its stream, so a slow create_link
cannot recreate a just-cleared row.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Stalker movie and series menu launches in MPV/VLC close the title's
own running external session first through the new shared
replaceOwnedExternalSession(), cancelling when teardown is not
confirmed or the selection moved on.
- The reset-progress row is disabled while an external player owns the
position, on the shared and the Xtream movie menus.
- Every ISO comma-bearing country name stays whole in provider lists.
- The dashboard hero spec competes a favorite movie for a slot so the
movie-title case is actually exercised.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Settings → Playback → "Play trailers in details background" (off by
default) hands the trailer embed URL to the detail shell; the new
app-hero-trailer-backdrop plays it muted and looping under the scrim
after three idle seconds with a corner mute toggle. It never starts under
prefers-reduced-motion or with saveData, and stops while the hero is off
screen or the window is unfocused. Xtream and Stalker movies and series
are wired; M3U has no trailer source.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Series pages take the same hero as movies: the provider's season marker
moves out of the title into a "Season N" chip, the status chip is localized,
the primary reads Continue / "S02E03 · 18m left" or Play / the next episode,
Trailer opens the modal, and favorites plus a "…" menu complete the row.
The menu carries mark season/series watched, reset progress, open in
external player, copy the next episode's URL, download season, show in
category and hide from Continue Watching — rows a provider cannot serve
are left out. The season container's header is "Episodes" with the season
pills and count; the season actions it used to show live in the menu;
episode cards are flat with a watched bar on the artwork, the plot and
"42 min · 18m left". Cast & crew and Similar follow the episodes. The
Electron and web E2E flows and the season container specs drive the new
menu and presenters; legacy detail styles are removed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Rebuilds the movie hero in the order kind label → title → chips →
description → resume bar → actions → credits. The backdrop fills the hero
behind a two-layer scrim and falls back to the blurred poster when the
provider has none or sends the poster twice. The action row is a single
line: the light primary with a two-line label (Continue · time left / Play ·
duration), a Trailer button that opens the modal, 44px icon buttons for
favorites, watched and download, and a "…" menu with other sources, open in
external player, copy stream URL, start over, reset progress and show in
category. Cast & crew and Similar (title + year) move into rows under the
hero; the bottom trailer section is gone. The multi-source E2E flow and the
release screenshot driver follow the menu.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(player): keyboard focus, contrast and ARIA for controls and settings
Dock and settings-panel icon buttons draw a 2px --pc-text ring on
:focus-visible, and Material's theme-coloured focus layer is off, so
keyboard focus shows on video in the light theme too. A focused selected
subtitle swatch now differs from one that is only selected.
Settings headings read --pc-text-secondary on denser glass
(--pc-glass-bg-dense, 0.86): 4.5:1 or more over mid-grey and white
frames. They wrap (overflow-wrap: anywhere, hyphens: auto), so long
German and Russian headings stay inside the sheet's heading column.
The settings panel is now radio groups only (SettingsRadioGroupDirective
over a CDK FocusKeyManager): one Tab stop per group on the checked option,
arrows, Home and End move focus without applying, and Space/Enter checks.
The dialog and its groups are named by real h2/h3/h4 headings, the
load-file action sits outside the subtitle radio group, the subtitle and
speed chips carry their value in their name ("Subtitles: English"), and
tune has aria-haspopup="dialog".
Adds a web E2E for the keyboard path in both themes with an axe check on
the open panel, and de/ru sheet heading wrapping; axe-core is a new dev
dependency for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): arrows check settings radios; subtitle chip reads On
Review follow-up:
- Arrow keys, Home and End now check the settings radio they reach, as a
native radio group does (the directive clicks it, so the template's
handler applies the choice); an option the engine already reports as
checked is not applied again.
- With subtitles on but no track marked selected yet (the engine can
report the switch before the track list), the subtitle chip reads and
announces "On" (new SUBTITLES_ON key, 19 locales) instead of "Off".
- The swatch row has 4px padding on every side, so the outer focus ring
is not clipped at the scroll edge of the panel body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): re-apply a settings radio while a switch is pending
The arrow-key check skipped any option the engine still reported as
checked. Arrowing from audio track A to B and back to A before the engine
confirmed B therefore sent no command for A, and playback ended on B with
focus on A. An arrow move always lands on an option other than the last
one applied, so it now applies unconditionally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): show the Up next card near the real end of an episode
The card appeared a fixed 8 minutes before the end: most of a short
episode, and far into the story of a long one, with no way to hide it.
- Adaptive lead: 4% of the episode, clamped to 40 s … 3 min.
- A closing-credits chapter in the last third, when timeline segments
carry one, brings the card forward to its start (capped at 5 min).
- Close button and Escape dismiss the card for the current next episode.
- After 10 s (not while hovered) the card collapses into a one-line pill.
- Seconds countdown in the last minute.
- New playback setting "Up next card" (default on) turns it off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): address Up next card review feedback
- Offer the Up next card setting for Embedded MPV only under the
frame-copy engine; native view never mounts the shared controls.
- Hovering pauses the collapse delay instead of restarting it.
- Document that the card stays visible when the controls auto-hide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): restart the Up next collapse delay for a new episode
- A card that stays mounted while its next episode changes gives the new
item the full delay instead of the previous item's leftover.
- The setting description no longer promises credit-based timing: no
current series path supplies chapters yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(playback): give the fullscreen side panel the settings panel's glass look
The fullscreen channel/episode panel was a full-height graphite strip while
the controls' settings panel is an inset, rounded glass card. The side panel
now wears the same surface: 16px inset (8px under 560px), 20px corners, the
controls' glass fill, hairline border, blur and shadow, the same open motion
and a 32px square close button. The edge hint uses the same glass, and the
episode list marks the playing row with the settings panel's selected cyan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): dismiss the side panel from the exposed edge gutter
The inset card leaves the hot zone's left strip visible beside it, above the
scrim, so a click there re-ran show() instead of dismissing. A completed
primary press in the zone now closes an open panel; hovering there still
counts as inside, since a hover-opened panel leaves the pointer resting in
that strip. The playing episode row also keeps its cyan tint on hover/focus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): decide an edge press from the panel state at pointerdown
A press held on the edge past the hover dwell opened the panel before the
release, which then read the open state and dismissed it. The press now
remembers whether it began on an open panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(settings): hide cached title matches the parental lock withholds
Cross-playlist title matches are cached by their consumers (Actor and
Discover routes, the dashboard trending and recommendation rails and
the four "similar in your portals" rails), so matches found while
unlocked kept advertising locked titles and their playlist names after
a relock. Each consumer now filters on read through a reactive
predicate on the match's provider category, so a relock hides them at
once and an unlock shows them again, without a re-query.
Closes#1723
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(settings): fall back to unlocked copies of a withheld title match
The dashboard trending and recommendation rails and the "similar in your
portals" rails picked one match per title at load, so hiding a withheld
match lost a copy of the same title in an unlocked portal. They now keep
every row the lookup returned and pick the match on read from the rows
the parental lock does not withhold. Adds the release note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(settings): derive the recommendations seed heading from the visible cards
The rail kept only the seeds that contributed a card to the original
selection, so a seed whose recommendations filled in after a relock (or
an offline prune) was missing from the "Because you watched" heading.
Every seed of the load is kept in order and the heading lists those that
contribute a card now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(playback): record recently viewed only after the stream plays
A channel, movie or episode used to enter Recently Viewed (and the
dashboard's Continue Watching hero) the moment it was selected or its
link was resolved, so streams that failed straight away cluttered the
history.
Writers now defer the write to a root PlaybackHistoryGate, keyed by the
stream URL and/or the playback session key. The inline players confirm
those keys once the owned engine's position has advanced by two seconds
(seeks, stalls, pauses and a previous stream's progress do not count),
the radio player does the same, and a launched MPV/VLC session confirms
on `opened`/`playing`. M3U with MPV/VLC configured keeps recording on
selection. Covers M3U (live, radio, movie detail), Stalker (live, radio,
VOD, series), Xtream VOD and series, and the global live collection.
The M3U host's embeddedPlayback is now compared by value: the history
write updates the playlist meta mid-playback, and a new but identical
playback object remounted the engine and restarted the stream.
E2E flows that relied on recording-on-click now play local fixtures
(HLS/TS/WebM routed in place of unreachable or public streams) and wait
for confirmed playback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): tighten recently viewed confirmation per review
- Correlate by session key first: when both the deferred write and the
confirmation carry a playbackSessionKey, only that is compared, so the
same stream URL played in another playlist no longer records a failed
attempt. URLs remain the fallback (portal writes, MPV/VLC sessions).
The M3U radio player now receives the host's session key.
- Count only playing progress: engines report `playing` (not paused, not
seeking) with each time update, so short seeks of paused media no
longer confirm a view.
- Xtream: a write confirmed after a playlist switch still saves to its own
playlist but no longer replaces the current playlist's recent list.
- Global live tab: a row confirmed after another row was selected still
moves to the top of an open Recently Viewed list (only a disposed tab
skips the notification).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): confirm session-keyed history only by its own session
A write deferred with a playback session key (M3U) is now confirmed only
by that key. The app-wide MPV/VLC session confirmation carries just the
URL, so opening the same stream externally from another playlist could
still commit an abandoned attempt. An "Open in MPV/VLC" recovery launch is
instead confirmed by the WebPlayerViewComponent that requested it, under
its own session key, once the launch has opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(playback): keep the history gate off the initial bundle
The `@iptvnator/services` barrel ships in the initial bundle, so adding
PlaybackHistoryGate there (and subscribing to it from the app-wide
ExternalPlaybackService) grew renderer.initialBytes by 1,141 bytes.
- Move the gate to a new lazy-only `playback-data-access` project
(`@iptvnator/playback/data-access`; scope:shared, domain:playback,
type:data-access) and register it in the coverage policy.
- The gate subscribes to MPV/VLC session updates itself; it is created by
the first deferred write, which precedes the launch it waits for.
ExternalPlaybackService is back to master.
- The Xtream "playlist switched before confirmation" check moves to the
lazy helper; the initial-path store only takes a `skipListRefresh` flag.
Net effect on this branch: +27 bytes over master (master itself is
108 bytes over the ratchet baseline already).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(playback): drop late Xtream confirmations off the initial path
The Xtream store ships in the initial bundle, so even the small
`skipListRefresh` flag cost 27 bytes there. A confirmation can only
arrive after a switch to another playlist from a slow MPV/VLC launch
(the inline player goes with the page), so the lazy helper now drops it
instead: recording it would misfile the item or replace the other
playlist's recent list. with-recent-items is back to master.
This branch is now 3 bytes below master on renderer.initialBytes; the
ratchet still reports master's pre-existing overage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(playback): pass the spec type-check gate from master
- playback-data-access: align tsconfig.spec.json with the epg-data-access
config #1705 updated (bundler resolution, global.d.ts for window.electron).
- M3U recent-history spec: type the selectSignal override.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): keep late Xtream confirmations in their own playlist history
A confirmation that arrives after a switch to another playlist (a slow
MPV/VLC launch) is no longer dropped: the lazy helper saves it to the
captured playlist through the data source, without reloading the store's
recent list, which belongs to the other playlist by then. The store and its
barrel ship in the initial bundle, so the save path stays in the feature
helper; renderer.initialBytes stays under the baseline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): correlate global live-tab history by its session key
The unified Favorites/Recent live tab deferred its history write by stream
URL only, so the same URL played from another playlist could confirm a
failed selection, and a switch to catch-up before confirmation could never
match. It now defers with the tab's playlist-scoped playbackSessionKey (the
key its players confirm with), and the tab's radio player receives it too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): keep MPV/VLC rows of the live tab confirmable by URL
The live tab's session key can only be confirmed by its own inline
players; MPV/VLC confirm the launched URL alone. A row that goes to an
external player (also later, after a double-click) now defers by URL, and
only rows played inline carry the session key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): per-channel M3U history attempts, capability-based Xtream fallback
- M3U: the recently-viewed dedupe key now includes the channel id, so a
second row of the same URL defers its own write (its session key) and
is recorded when it plays after the first row failed.
- Xtream late write: key uncached content by Xtream id per
supportsXtreamSqliteDataSource (the data-source factory's contract), not
by a generic Electron bridge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* build(test): make spec tsconfigs resolve what Jest resolves
Lib spec tsconfigs used module: commonjs with node10 resolution, which cannot
see Angular's exports-only secondary entry points, and dropped global.d.ts, so
tsc reported thousands of resolution errors and no window.electron typing.
Switch them to module: preserve with bundler resolution (ts-jest still forces
CommonJS emit outside ESM mode), add global.d.ts to every spec program, type
jest.unstable_mockModule for the ESM workspace, include the ui-epg and
ui-playback specs that jest.web-esm.workspace.ts runs under the web spec
config, and drop the snack-bar stub that shadowed the real Material types.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* ci(test): gate spec type-checking with typecheck:spec
Add tools/typecheck/spec-typecheck.mjs, which runs tsc --noEmit over every
tsconfig.spec.json with a small pool and fails on any diagnostic, wire it into
the unit-and-typecheck job after typecheck:ci, and document the gate and the
spec tsconfig conventions in the validation map. Also bring the non-Tier-A
spec configs (remote-control-web, ui-remote-control, stalker-mock-server) to
the same conventions so the gate covers the whole workspace.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: fix the spec type errors surfaced by typecheck:spec
With the spec programs resolving modules and ambient typings correctly,
tsc reported 432 genuine errors across the Tier A projects: read-only
capability flags assigned on Partial<> doubles, signal-store values used as
types, fixtures missing required fields, index-signature property access,
partial bridge doubles cast through incompatible shapes, and deferred
resolvers narrowed to never. Type the doubles instead of casting to any:
writable mapped types for capability flags, InstanceType<typeof StalkerStore>,
typed jest.fn signatures, protectedState: false on test signal stores, and
completed fixtures. Production changes are limited to bracket access for
index-signature properties under the libs' noPropertyAccessFromIndexSignature
setting and two narrowing guards in the global favorites loader.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(playback): use the ESM setup's jest global in the controls fixtures
The fixture imported jest from @jest/globals, which is not a direct
dependency. Jest provides that module at runtime, so tests passed, but on a
clean pnpm install tsc cannot resolve it and typecheck:spec failed in CI.
The ESM test setup already installs import.meta.jest as the global, typed
by @types/jest, as the other ESM specs use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: type the parental lock doubles merged since the gate was written
The parental lock feature (#1601) and the Stalker actor route landed on master
with spec doubles declared as zero-argument jest.fn()s that the tests then
drive with the real arguments, plus a copy of the ResizableDirective override
imported from a library that does not export it. Give the doubles the lock
service's real signatures, drop the dead override as in the sibling layout
specs, use bracket access for the actor route's personId param, and keep the
Stalker layout spec within the 1200-line limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Tier A coverage runs projects a few at a time (largest first, bounded Jest workers, buffered output, fail-fast kept) and ts-jest transpiles with isolatedModules instead of type-checking per process; five type re-exports become export type, two decorated inputs use import type. Unit Tests and Typechecks job: 26 min -> 9 min (Tier A step 23 min -> 6.5 min).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First step of the performance-journeys ratchet (plan thread: J1 `launch`, counter `renderer.initialBytes`).
- `tools/performance/measure-initial-bytes.mjs` reads the built `dist/apps/web/index.html` and sums `index.html` plus every same-origin `<script src>`, `<link rel="stylesheet">` and `<link rel="modulepreload">` it references. Manifest, icons, external URLs and lazy chunks are not counted. A referenced file missing from the build fails the measurement instead of counting as zero bytes.
- `--json` prints the breakdown; `--summary <file>` writes the `journeys.<journey>.counters` shape a ratchet checker will consume (next PR).
- New Nx project `performance-tools` (test + lint targets), Tier B in `tools/coverage/coverage-policy.json`, root scripts `perf:initial-bytes` and `perf:tools:test`.
- New contract `docs/architecture/performance-journeys.md`, linked from the validation map, the agent context map and the README.
- **Review follow-ups:** resources are deduplicated by request URL (query kept, fragment dropped); `index.html` is parsed with parse5 (already a repository dependency, scripting enabled), so comments, bogus comments, raw-text bodies (script/style/noscript/title/textarea), inert `<template>` contents and character references in attributes all follow the HTML5 algorithm instead of a hand-written scanner; the review's edge cases stay as regression tests; docs show the `pnpm --silent` form for JSON output and explain how the counter relates to Angular's rounded "Initial total".
- **Found while measuring:** the environment files and the playback diagnostic panel imported the whole `package.json` (`import packageJson from '@package'`), which esbuild cannot tree-shake, so `main.js` carried the complete file and the counter moved with every script or dependency edit. They now import `{ version }` only (eb662c485): `main.js` shrinks by **11,539 bytes** and the counter no longer depends on `package.json`. Jest's ESM loader exposes JSON only as a default export, so the two web Jest configs map `@package` to a stub that serves the real file's fields as named exports. Release note: `.changes/web-version-only-from-package-json.md` (`type: perf`).
Production build after this PR measures **2,739,508 bytes** (11 files + `index.html`); Angular's "Initial total" is this minus `index.html` and `assets/app-config.js`. The baseline file and the CI check land in the follow-up PRs; C1 (lazy Angular date locales) then lowers it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups
Follow-ups to the season posters shipped in #1628:
- The >6-seasons dropdown (`SeasonTabsComponent`) now carries a 28×42
season thumbnail at the start of each menu row that has a poster and in
the closed trigger for the selected season, fed by a new `seasonPosters`
input from the season container and the fullscreen episode panel. Rows
without a poster get no placeholder, a failed image is dropped, and the
pill row stays text-only as the design review decided.
- The fullscreen season strip's episode count uses its own
`PORTALS.EPISODE_COUNT_ONE/OTHER` keys instead of borrowing the download
manager's; all 18 locales filled through the i18n merger from their
existing `DOWNLOADS.EPISODE_COUNT_*` translations.
- The Stalker mock's serve targets no longer pin `PORT` (an nx:run-commands
`env` entry overrides the shell), and `main.ts` resolves `PORT`, then the
Playwright-side `MOCK_PORT` alias, then 3210 — so `MOCK_PORT=3310` now
relocates the whole E2E run. The Xtream mock honours `XTREAM_MOCK_PORT`
the same way.
- `resolveAutoSelectedSeason` gets a direct spec covering every branch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream-mock): mint marketing asset URLs on the port the server bound
Greptile P1 on #1633: the listener honoured `XTREAM_MOCK_PORT`, but
`marketingAssetOrigin()` still read `PORT` alone, so a run relocated only
through the alias sent every poster/backdrop/logo/episode URL to 3211.
One resolver (`resolveXtreamMockPortString` in `mock-port.ts`: `PORT`,
then `XTREAM_MOCK_PORT`, then 3211) now feeds the environment parser, the
marketing asset origin and the demo-guide origin fallback. A spec pins the
precedence and that `marketingAssetUrl` follows the bound port.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Series detail pages now render the selected season's poster as a season
cover next to the season tabs and description, and the fullscreen episode
panel shows the same poster as a season strip above its tabs.
Resolution is TMDB-first, like the show artwork merge: the lazy season
enrichment stores `/tv/{id}/season/{n}` `poster_path` as a w342 URL in
`tmdb_season_posters` (Xtream) or `StalkerSeriesTmdbSeasonsService.posters()`
(Stalker), under the same write-only-if-changed convergence guard as the
season overview. Xtream falls back to the provider's `seasons[].cover_big`/
`cover` when it is an http(s) URL other than the show poster, because panels
repeat the show poster on every season. Stalker is TMDB-only.
The cover column is not rendered for one-season items, seasons without a
poster, or a failed image, so every fallback is today's markup. It is sized
by a new `--season-cover-width` token (96/120/144px per Settings.coverSize).
The hero poster never follows the season.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Movie and series detail pages showed two arrow_back controls while the
inline player was open: the shell's sticky arrow (added in #1576 so Back
survives scrolling) meant "Close player" in watch state, while the
now-playing bar carried a second arrow that meant route-level Back — the
same icon with two meanings, next to a "Close player" button that
duplicated the first.
The shell's sticky arrow is now route-level Back in browse and watch
alike, and the bar carries no arrow of its own. Closing the player is
the bar's "Close player" button and Escape, which still unwinds one
level (close, then back). Hosts without a browse Back target (M3U,
downloads) render no arrow in either state.
Unit specs for the shell and the inline player cover the new contract;
the Electron and web E2E helpers that pressed Back from watch are
updated, and the M3U flow closes the player through the bar's button.
Docs, the mirrored CLAUDE.md/AGENTS.md paragraph and a release note
follow the change.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Series playing in fullscreen get the same slide-in side panel the live channel list has, with season tabs and the episode list: rest the mouse on the left edge, click it, or press C; pick an episode and it plays inline without leaving fullscreen.
- Panel contract: `FullscreenChannelPanelHost` gains optional `panelSearchEnabled` and `panelKind`; the template context gains `open`. Pointer/keyboard rules and the four live providers are unchanged.
- Series host: `PortalInlinePlayerComponent` provides the token through `createEpisodePanelHost()` and stamps `app-fullscreen-episode-panel` (SeasonTabsComponent over rows with TMDB still or numeral tile, label, runtime, clamped overview, progress, watched check, now-playing marker; playing row centred on open). Episode clicks reuse the Up Next rail's inline path; season tab clicks reach the hosts' `onSeasonSelected` (Xtream TMDB season enrichment, Stalker lazy VOD load with a Retry row after a failed request).
- Gates: `Settings.fullscreenChannelPanel` (label now covers both lists in all locales), episode content only, native-view Embedded MPV withheld by the view, external players excluded.
- Inline-series e2e moved to `xtream-series-playback.e2e.ts` with shared Xtream helpers in a fixture; adds a fullscreen episode switch through the panel.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(playback): make the fullscreen channel panel discoverable and calmer to close
The left-edge hot zone was an invisible 28px strip with nothing telling the
user where the list lived, so "hovering the left side" rarely reached it while
`C` always worked. Mouse movement over the stage now reveals a slim edge hint
tab (CSS chevron, fades after 2.5 s idle, lit while the pointer rests in the
zone), the zone grows to 40px (48px coarse), and a click or tap on the edge
opens at once without the dwell.
Closing follows the pointer more honestly: the mouse-leave grace grows from
420 ms to 1 s, and it applies only once the pointer has engaged with the
panel, so a `C`-opened list no longer closes while the mouse merely roams over
the video under the user's typing. Clicks inside the panel never close it.
Found on the way: closing with Escape while the mouse still rests on the edge
reopened the panel 160 ms later, because Chromium synthesizes a `pointerenter`
on the hot zone the aside slid away from. An explicit close now re-arms the
zone only on the next real pointer move.
Verified live over CDP on HTML5, Video.js, ArtPlayer and Embedded MPV
frame-copy; the hover mechanism itself was sound on every engine.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(playback): open the panel only on a primary click begun on the edge
Review follow-up. The hot zone opened on every pointerup, so a drag released
over the edge, a right or middle click and a pen barrel button all opened the
panel; now pointerdown records the primary pointer and pointerup must match
it (a leave or cancel forgets the press). The synthetic pointerenter that
follows an explicit close no longer arms the hint either: the edge stays
clear until the next real pointer move, which arms it and starts the dwell.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* chore(deps): upgrade Angular to 22.1 and Nx to 23.2
* fix(deps): complete Angular migrations after rebasing on master
* fix(ci): use the Node pin for Windows runtime refresh
* docs(deps): synchronize the workspace-shell Node requirements
* docs(epg): add programme guide redesign spec for the M3U host
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): add programme guide implementation plan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add window-scoped guide programme queries
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): harden guide query scoping, caps and row mapping
- Scoped guide programme/coverage queries now include legacy
(unsourced) rows via source_url IN (...) OR IS NULL OR '',
mirroring EpgQueryService's legacy fallback.
- getProgramsForChannels/getProgramCoverage build their result from
the normalized, capped window.channelIds instead of the raw
request, so a key cut by the cap is absent rather than [] — an
invalid window now returns {}. Truncation logs counts only.
- Split the 100-channel guide cap from a new 2000-key coverage cap,
and cap sourceUrls at 50; normalizeGuideWindow takes the cap as a
parameter and moved (with guideWindowOverlapSqlText) into
epg-guide-window.util.ts.
- Extracted shared row mapping (toEpgProgramFromRow/isValidEpgProgram)
into epg-program-row.util.ts, used by both EpgQueryService and
EpgGuideQueryService so invalid start/stop rows are dropped
identically in both.
- Added a real-SQLite-backed test for the overlap predicate's exact
text, plus per-key array copies in the response.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): render the guide predicate in tests and document its scope
Correct the guide query's JSDoc: it runs one query accepting the union
of requested-source and unsourced legacy rows, unlike EpgQueryService's
two-query scoped-then-legacy fallback. Replace the hand-maintained
plain-SQL twin of the Drizzle overlap predicate with a rendered copy of
the real predicate (SQLiteSyncDialect().sqlToQuery) in the spec, add a
source-scoping case, and drop the now-redundant operator-sequence test.
warnIfTruncated reuses uniqueTrimmedStrings and names which read
(programme/coverage) was truncated. Rename epg-query.service.ts's local
EpgProgramRow to EpgProgramSelectRow so it isn't confused with the
shared EpgProgramRow type, and document getProgramCoverage like its
sibling.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): expose guide programme and coverage reads over the bridge
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): separate coverage chunk size in the guide plan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add guide source contract, day layout maths and preferences
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): key guide IPC answers by trimmed, present keys only
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): guide search hits carry a row id
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): make guide geometry DST-safe and tighten the contract
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): cache guide programmes per day with batched loading
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add guide keyboard navigation controller
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): make guide programme cache robust to first-run effects and coverage failures
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add the programme guide grid components
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(epg): add a Guide button to the timeline toolbar
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(m3u): adapt the playlist channel list to the guide contract
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): guard the guide's initial group scope and track language changes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(m3u): open the programme guide in place with a docked player
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): scope guide keys to the grid, clip the now-line and re-measure on resize
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(epg): remove the multi-EPG overlay and the channel-range IPC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): document the programme guide and its release note
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* i18n(epg): translate the programme guide
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): let the guide own the keyboard and gate its entry points
While the programme guide is open the docked player carries
`data-player-shortcuts-suspended`, which `ControlsShortcuts` now honours
alongside `[inert]` — the arrows moved the player's volume instead of the
guide's row focus. The external-player strip loses its Collapse toggle
(nothing to reveal, no preference to write), the header action and its
palette command report `disabled` when the guide cannot open, the docked
strip derives its programme from the active channel's own schedule instead
of the retained NgRx value, switching playlists closes the guide, and the
collapsed strip can reach 48 px on phones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): keep the sidebar mounted while the guide is open
Guide mode wrapped the sidebar in `@if (!guideOpen())`, so opening the
guide destroyed `app-channel-list-container`, whose `ngOnDestroy`
dispatches `resetActiveChannel()`. That cleared the active channel, which
unmounted the block hosting `app-epg-guide` and tripped the
`!canOpenGuide()` effect into closing the guide again: the guide never
appeared and the page dropped to "Please select a channel".
The sidebar now stays mounted and is hidden with
`.sidebar--guide-hidden` plus `inert`, so it is neither focusable nor read
by assistive technology while the guide owns the layout. Hiding also
preserves the channel list's scroll position across guide toggles.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(e2e): cover the programme guide flow
Imports a two-channel playlist with XMLTV, opens the guide from the
timeline toolbar and asserts the row list, the "Only with EPG" filter, a
channel switch that keeps the guide open, the hidden-but-mounted sidebar,
and that the player element survives both the mode and channel switches.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* chore(epg): tidy guide docs, palette gating and the unbound output
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): match guide favorites by channel URL and skip re-activating the playing row
Favorites are persisted by channel URL (FavoritesActions.updateFavorites),
so the Favorites scope compared the wrong key; the id stays as a legacy
fallback. A double-click arrives as click, click, dblclick and each
activate restarts playback, so the guide now leaves the already-playing row
alone and the commit path only closes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* perf(epg): let the guide window predicate use the programme time index
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): stabilise guide row identity, seed the sidebar group and provide translations in every player fixture
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(epg): split the guide shell, add a roving focus model and offset-aware search times
The shell component now owns rows, focus and the viewport only: the day,
zoom, density, filters, clock and day geometry move to EpgGuideViewState,
and every programme-dialog entry point to EpgGuideDialogController.
Keyboard navigation is reachable by assistive technology: exactly one grid
cell carries tabindex="0" (the focused cell, else the playing row's channel
cell, else the first row's), the guide moves DOM focus with it after each
handled key, a click hands the roving index to the clicked cell, and the
viewport, rows and cells expose grid/row/gridcell roles.
Search results were formatting raw provider instants, so they ignored the
EPG display offset; they go through getProgramTimeMs like every other time
the guide renders.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(m3u): make guide row ids collision-proof and gate the G shortcut
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): keep guide keys on the grid, reconcile focus with filtered rows and wrap the toolbar
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(epg): describe guide row ids as scope-local
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): clear guide search on scope change, match the active duplicate by url, keep failed coverage unknown
Search hits carry scope-local row ids, so a scope change drops them.
Two playlist entries can share an id but not a stream, so the active row
is matched by id + url before falling back to the id. A failed coverage
query now rejects instead of answering an empty set, which the guide
already treats as "coverage unknown" (every row stays visible).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): tell duplicate guide rows apart by group, keep G out of dialogs, use prototype-safe answers
The store spreads the selected channel, so the active row is matched by
id, url, group and name before widening; G no longer closes the guide from
a dialog or menu; guide answers use null-prototype records so a key named
__proto__ stays an own property.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): let coverage reject on lookup failures and compare whole entries for the active guide row
EpgQueryService.getChannelMetadata swallowed database errors into {}, so the
guide's coverage read could publish an empty set after a transient failure;
the guide now uses the strict resolveChannelMetadata (getChannelMetadata is
the fail-soft wrapper around it). The active guide row is matched on the
whole channel entry (all fields except the reducer-rewritten epgParams)
before widening to url and id, so copies that differ only in playback
headers or logo are told apart.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): let the guide return catch-up to live and normalise programme-search rows
The guide source contract gains an optional livePlayback signal: while the
host plays a catch-up URL, the active row may be activated again, which is
how the M3U host returns to live. EPG_DB_SEARCH_PROGRAMS now maps the raw
snake_case rows to the EpgProgram shape the bridge promises (plus the joined
channel name), so search hits resolve their channel and keep descriptions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): name search hits, keep guide coverage strict on mapping failures
- Search results and the unresolved programme dialog show the channel's
display name (playlist row name, else the XMLTV display name the search
joined in) instead of the raw XMLTV id.
- The guide coverage read resolves manual mappings through a strict variant
that rejects on database failure, so a mapped channel can never be reported
as uncovered and hidden by "Only with EPG".
- Architecture doc describes the tiered active-row resolution.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(epg): offer the Guide action in the list view too
The EPG list view mirrors the timeline's input/output contract, but the Guide
action was bound only in the timeline branch, so Settings → EPG → Guide view =
List lost the in-panel entry point. The list toolbar now carries the same
icon-only Guide button behind `guideAvailable`/`openGuide`, and the M3U
host binds it in both branches.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): try advertised TS after initial web HLS HTTP failure
* refactor(playback): extract fullscreen channel panel state
* test(xtream): keep synthetic media within the mock project
* fix(playback): apply themes to player and EPG panels
* test(playback): verify active recording icon theme
* fix(epg): keep loading shimmer visible in both themes
* fix(playback): close legacy picture-in-picture on video replacement
* test(playback): wait for the selected video before PiP setup
* test(playback): await changed settings before PiP navigation
* fix(playback): release legacy WebKit picture-in-picture
* fix(playback): seek Embedded MPV steps relative to mpv's own position
Arrow keys and the ±10 s buttons in the Embedded MPV player advanced only
about a second per press when pressed repeatedly or held. The shortcuts
already asked for 5 s steps, but `EmbeddedMpvCommandRunner.seekBy` turned
each step into an absolute `seek` computed from `session.positionSeconds`,
which is floored to whole seconds, polled every 500 ms (helper snapshots at
most every 250 ms) and not refreshed by the seek reply. Every press inside
that window therefore landed on the same target.
Steps now go through a new `EMBEDDED_MPV_SEEK_BY` IPC / `seekEmbeddedMpvBy`
bridge method that every backend forwards as mpv `seek <delta>
relative+exact`: `seekBy` exports in the macOS addon and the Windows/Linux
`wid` addon (Linux over its JSON IPC socket), and a `seek-by` stdin command
in the frame-copy helper. mpv resolves the delta against its own position
and merges queued relative seeks, so presses accumulate as in mpv itself.
The absolute form survives only as a fallback for a preload without the
method or an addon binary without `seekBy`; the timeline scrub still
commits an absolute target.
Validated with a real mpv 0.39 IPC probe: three relative seeks in a burst
advance +15 s, three absolute seeks from one stale base advance +5 s.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(playback): drop speculative position update from relative Embedded MPV seeks
Review follow-up for the relative seek path.
The macOS and Windows/Linux `seekBy` exports advanced `snapshot.positionSeconds`
by the delta after dispatching the mpv command. That is not idempotent the way
the absolute seek's optimistic write is: the observer (mpv event thread, or
the Linux IPC poll) can already have stored the post-seek `time-pos` under the
same mutex, so adding the delta on top counted the step twice, and while paused
nothing corrected it. On Linux it also advertised a position that a failed
socket delivery never reached. Relative steps now leave the snapshot alone;
only the observed `time-pos` updates the position.
The packaged Linux frame-copy smoke now drives `seekEmbeddedMpvBy` through the
built app: a burst of three +2 s steps issued without waiting for snapshots has
to land on 6 s, and a -60 s step has to clamp at 0. The generated Y4M fixture
grows from 2 s to 12 s (about 415 KB) so the burst and the playing section that
follows stay inside the clip. Replayed against a local mpv 0.39 with the same
fixture and media server: burst -> 6.0, -60 -> 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(agents): mirror the Embedded MPV relative-seek contract into AGENTS.md
Review follow-up: the Shared Player Controls section documents the frame-copy
commands and shortcuts, so the relative seekEmbeddedMpvBy invariant lives
there too, next to the CLAUDE.md note.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(playback): reject a Linux relative seek the mpv IPC socket did not accept
Review follow-up: the Linux branch of SeekBy discarded the socket transaction
result and returned normally, so a step that never reached mpv looked like a
seek still awaiting observation. It now throws like a failed mpv_command_async
on the in-process engines; the renderer swallows the rejection and resyncs
from the next snapshot, and the main process logs it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Follow-up to #1516 for the vendor-chrome path (shared controls opted out). With
Video.js's own controls, Chromium leaves a clicked control-bar button focused,
and a focused Video.js component captures the keyboard entirely, so after
clicking fullscreen Space left fullscreen instead of pausing and the seek,
volume and mute keys did nothing until the user clicked the video. ArtPlayer
and the native HTML5 controls were verified unaffected.
The legacy Video.js chrome now releases the focus a pointer interaction leaves
on a control (vjs-pointer-focus-release.ts). The release is scoped to the
.vjs-control-bar and pointer-attributed, and runs on both focusin (focus
landing on a control, e.g. a menu handing focus to its button) and click (a
control clicked while already focused, which fires no focusin); keyboard Tab
focus and modal-dialog focus traps are preserved. The eligibility helper is
shared with ControlsSurface via pointer-focus-release.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Extra libmpv options (Settings > Playback) reach every embedded engine off the command line (createSession array on Windows/macOS, a 0600 --include file on Linux native-view, a stdin preamble for the frame-copy helper); the keys the embed depends on are refused, and keys libmpv rejects are reported once per session. Dropped streams reload automatically (error, or ended on live) with 2 s -> 30 s backoff, six attempts per outage and a 30 s stability reset, only for a load that already played; engine failures stay terminal, a running recording is filed as interrupted and restarted after the reload, and an external subtitle file is re-added. Settings.embeddedMpvAutoReconnect (default on) opts out; the player shows 'Reconnecting... attempt N of M'.
Started by Bpl5966 in #1515 and finished by the maintainers in the same PR.
Co-authored-by: Bpl5966 <amine.b1959@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Chromium focuses a clicked <button>, and a focused control captures the
keyboard: Space and Enter activate it again, and ControlsShortcuts yields
to any interactive element in the key's path. After a click on the
fullscreen button, Space left fullscreen instead of pausing and the seek,
volume and mute keys did nothing until a click on the video took focus
away. Follow-up to #1512, which stopped that focus from pinning the bar
but left it on the button.
A completed pointer click now releases the focus it left on the control
(onBarClick -> ControlsSurface.releasePointerFocus). The click is
attributed by its pointerType (empty for Enter/Space activation and
element.click()), with the legacy MouseEvent fallback answered once per
recorded press, so keyboard activation keeps focus where Tab put it.
Only buttons and range sliders are released. Chromium keeps its
sequential-focus starting point at the blurred control, so a later Tab
continues from it. The release dispatches a focusout while the pointer
still rests on the control, so the volume anchor ignores it instead of
closing the popover under the hovering mouse.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Chromium focuses a clicked <button>, so the shared controls bar treated every
mouse click on a control (fullscreen, mute, ...) as keyboard navigation and
pinned itself open until a click on the viewport took focus away — a click
that also paused playback. Most visible on Embedded MPV frame-copy after
entering fullscreen; reproduces on HTML5, Video.js and ArtPlayer too.
Only keyboard-originated focus pins the bar now: pointer-attributed focus
reveals without a pin, the press record is discarded on the first bar focus
event or any keydown, a pointerdown inside the bar releases a keyboard pin,
and a keydown bubbling out of a bar control re-pins it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The HTML5 player chose its engine by exclusion: anything that was not
mpd/ts/mp4 went to hls.js, so .mkv (the default Xtream series/VOD
container), .webm, .avi, .mov and .m4v were fed to hls.js as manifests.
hls.js raised a manifest error and the player showed the network/provider
diagnostic over media Chromium plays natively.
Add resolvePlaybackUrlSourceKind() to @iptvnator/playback/util as the one
URL-to-engine rule (mpd -> dash, m3u8/m3u -> hls, ts/m2ts/extension-less
-> mpegts, everything else -> native) and read it from both the HTML5
player and ArtPlayer's getArtPlayerVideoType(), so the two engines agree.
ArtPlayer serves every native container through a single
ART_PLAYER_NATIVE_SOURCE_TYPE custom type, keeping the source session the
owner of teardown and controls binding. The HTML5 native <source> carries
the video/mp4 hint only for MP4-family files: a hint the browser's
canPlayType() rejects makes it skip the source, and Chromium demuxes
containers it does not advertise there.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
WebPlayerViewComponent remounts the engine component for every playback
application, and the DOM Fullscreen API exits the moment its element leaves
the document. The fullscreen element was the engine shell, so every next-
episode click, autoplay hand-off, channel zap and alternative-source switch
dropped the viewer back to the page.
app-player-controls gains a `fullscreenTarget` input; HTML5, Video.js,
ArtPlayer and Embedded MPV forward it, and WebPlayerViewComponent passes its
own host element, which spans all applications of one mount. Keeping
fullscreen exposed a latent bug: the Electron header handoff set plain
fields under OnPush hosts and was only rendered thanks to the fullscreen
exit's stage resize; `channel`/`vjsOptions` are signals now.
Covered by unit regressions (fullscreen target, WebPlayerView remount, OnPush
handoff), a web-e2e run through a manual and an automatic episode switch, and
a manual Electron check. Docs and release note updated.
Closes#1498
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>