4grayandClaude Fable 5.1 eb96b4c9f2 feat(website): turn the screenshot showcase into a channel switcher (#1540)
* feat(website): turn the screenshot showcase into a channel switcher

Second landing redesign PR. "See it in action" was a tab strip in a
dashed frame showing one screenshot inside a drawn window chrome that
duplicated the chrome already in the shot. It is now a channel list:
number, screen name and one line about what the screen is for on the
left, a single frame on the right, and a caption linking to the matching
feature page.

- Channels advance on their own with a progress hairline under the
  active row; hovering, focusing or scrolling the block out of view
  pauses it and `prefers-reduced-motion` disables autoplay entirely.
- A brief on-screen "CH 03" badge confirms every switch.
- Arrow keys, Home and End move between channels; the list is a proper
  vertical tablist with roving tabindex, panels carry `aria-hidden`.
- Six screens: Dashboard, Live TV, Program guide, Movies & series,
  Downloads, Settings. Same files from `public/screenshots`; the
  add-playlist shot is replaced by the movie detail and the download
  manager.
- On phones the screen comes first and the list follows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(website): cover the channel switcher, keep focus pausing after mouseleave

Hover and focus now pause autoplay independently: clicking a channel
focused it, but moving the pointer away resumed the timer and seven
seconds later the selection moved under a still-focused tab.

New `website-screenshot-showcase.test.mjs` (in the website test target):
a structural half over the built HTML (vertical tablist, roving
tabindex, one aria-hidden panel per channel) and a browser half that
serves the build and drives it in Chromium — autoplay advances, hover
pauses, click + mouseleave keeps the pause while focused, arrow/Home/End
keys move selection, focus, panel, caption and badge together, blur
resumes, and `prefers-reduced-motion` disables autoplay. The browser
half falls back to the system Chrome channel and skips when no Chromium
exists, so the structural checks run everywhere.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(website): lazy-load every showcase screenshot

The block sits below the hero and the feature grid, so an eager first
frame only competed with above-the-fold assets for visitors who never
scroll to it. Native look-ahead loading brings it in well before the
switcher is on screen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(website): share the browser harness, fail in CI without Chromium

`website-browser-support.mjs` owns the loopback static server and the
Chromium launcher for the website browser tests. The server resolves
every request against the build root and answers 404 for anything that
escapes it (CodeQL js/path-injection on the previous inline copy). The
launcher tries the Playwright download, then the system Chrome and
Chromium channels; when none launches it returns null locally so the
structural half still runs, and throws under `CI` so the interaction
assertions can never turn into a silent skip on the runner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(website): defer inactive showcase frames, document the browser tests

The six frames are stacked with opacity, so native lazy loading treated
all of them as near-viewport and fetched every screenshot at once. Only
the first frame now ships with a `src`; the switcher assigns it from
`data-src` when a channel is shown and preloads the one after it, so at
most two frames are ever in flight. The showcase test asserts the
markup and the runtime behaviour.

The website README now describes the browser-dependent suites, the
shared harness, and the skip-locally / fail-in-CI rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(website): describe only the suite this PR adds

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): resume the switcher from where it paused, keep DOM order on phones

The dwell clock now stops while the switcher is hovered or focused and
the pause time is added back on resume, so the progress hairline
continues from its frozen position instead of snapping to zero and
granting a fresh seven seconds. The test asserts the resume.

On phones the list stays before the screen in the DOM and on screen
(tabs before their panels, focus order equals reading order); it hides
the per-channel descriptions and the keyboard hint there so the screen
stays close instead of being reordered with `order-first`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): never crop a showcase frame

The screen box stretches to the channel list's row height, and with
`object-cover` a wide screenshot lost its right side just above the
`lg` breakpoint. Frames are now contained on a dark stage (a letterbox,
as on a TV), and the per-channel descriptions are hidden between `lg`
and `xl` so the row stays close to the frame's own height.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): pause the switcher dwell while offscreen instead of resetting it

Leaving the viewport is now a pause like hover and focus: the frame
loop stops, the progress hairline keeps its width, and the clock
resumes from the same mark when the block scrolls back in. Only a
channel change resets the dwell. The interaction test scrolls away and
back to assert it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(website): no next-frame prefetch when reduced motion disables autoplay

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(website): persistent pause control for the channel switcher

Hover and focus only pause while they last, which is no use to touch or
screen-reader visitors, so the list footer now carries a Pause/Resume
toggle (`aria-pressed`, full `aria-label`) that keeps autoplay stopped
until pressed again (WCAG 2.2.2). It is removed under reduced motion,
where nothing advances.

The interaction test now waits for the seven-second rollover and checks
that tab, panel, caption, badge and the preloaded frame all move to
channel 02, and that the toggle holds through mouseleave and blur.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): keep the tablist to its tabs, no successor prefetch when autoplay is off

The channel list's header and the Pause/Resume control sat inside the
`role="tablist"` container; the tablist now wraps only the six tabs so
assistive technology reads the toggle as an ordinary button beside the
list. `show()` preloads the next frame only while autoplay can reach it
(neither reduced motion nor the toggle has stopped it). Tests assert
both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): preload the successor when auto-advance resumes

A channel picked while the switcher is paused deliberately skips its
successor; resuming now fetches that frame so the next automatic switch
does not land on a blank screen. The interaction test covers
pause → manual selection → resume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): Resume restarts autoplay at once; same-channel progress in the test

The transient hover/focus pauses now watch only the channel list and the
screen. The footer with the Pause/Resume control is not one of those
regions, so after pressing Resume — with the pointer and the focus still
on the button — autoplay visibly restarts instead of waiting for the
visitor to leave the whole block.

The interaction test compares progress within one channel (a paused
Movies before and after Resume) rather than across channels, which
could fail on a slow runner, and asserts that autoplay runs while the
toggle keeps focus and hover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* perf(website): one scheduler for the switcher, no frames while paused

Every pause reason (hover, focus, the toggle, leaving the viewport) now
goes through a single `sync()`: the animation-frame loop runs only while
the block is visible and nothing pauses it, and is cancelled otherwise,
so a switcher left paused schedules no frames at all. The dwell clock
still resumes from where it froze, and the successor frame is fetched
only when the loop actually starts — so scrolling away and back under a
persistent pause loads nothing. Tests count scheduled frames while
paused and cover pause → manual selection → offscreen → return.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): a channel picked while paused gets its full dwell after Resume

`show()` reset the pause mark, so the time a visitor spent paused after
picking a channel counted toward that channel's dwell and Resume could
advance immediately. When the loop is not running the new channel now
starts out paused at that moment. The interaction test asserts the
progress is still near zero right after Resume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): follow reduced-motion changes and hidden tabs in the switcher

The reduced-motion preference is read from a live MediaQueryList: when
it changes while the page is open the scheduler stops or restarts and
the Pause/Resume control is hidden or shown (it is hidden, not removed,
for that reason). A hidden document counts as a pause too — background
tabs throttle animation frames while the clock keeps running, so
without it the first frame back would skip a channel. The interaction
test flips both at runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(website): plain action button for the switcher pause, no manual animations under reduced motion

`aria-pressed` on a button whose name changes between "Pause
auto-advance" and "Resume auto-advance" announced the wrong thing; the
control is now an ordinary action button whose name says what pressing
it does next. The OSD slide and the panel fade get
`motion-reduce:transition-none`, so a manual selection under reduced
motion moves nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 23:58:24 +02:00
2019-10-06 17:34:49 +02:00
2023-01-23 22:42:23 +01:00
2026-08-30 10:05:26 +02:00
2020-09-27 20:33:37 +02:00
2025-10-07 18:02:13 +02:00
2021-02-07 00:19:57 +01:00
2020-09-27 20:33:37 +02:00
2020-09-27 20:33:37 +02:00

IPTVnator - IPTV Player Application

IPTVnator icon

Release CI status Releases Codecov Telegram Bluesky

🌐 Website | Telegram channel for discussions | Buy me a coffee | GitHub Sponsors

IPTVnator is a video player application that provides support for IPTV playlist playback (m3u, m3u8). The application allows users to import playlists using remote URLs or by uploading files from the local file system. Additionally, it supports EPG information in XMLTV format which can be provided via URL.

The application is a cross-platform, open-source project built with Electron and Angular.

⚠️ Note: IPTVnator does not provide any playlists or other digital content. The channels and pictures in the screenshots are for demonstration purposes only.

Important

Official sources only. IPTVnator is a free, open-source player — it never sells IPTV subscriptions, channels, or playlists. Websites offering "IPTVnator subscriptions/channels/premium/activated" builds are not affiliated with this project. Get the app only from the official website or GitHub Releases. See Beware of unofficial IPTVnator websites and IPTV services for details.

IPTVnator: Channels list, player and epg list

Features

Playlists & sources

  • M3U / M3U8 playlists from local files or remote URLs 📂, with automatic updates on startup
  • Xtream Codes (XC) and Stalker / Ministra (STB) portal support
  • Custom "User-Agent" header per playlist

Playback

  • Built-in HTML5 player (HLS.js or Video.js) with a resizable, resumable inline view
  • Optional unified IPTVnator controls for HTML5, Video.js, and ArtPlayer, enabled in Settings → Playback (experimental)
  • External players — MPV, VLC, and IINA on macOS (mpv.app / VLC.app bundle paths supported) (desktop)
  • Embedded MPV — native mpv rendered inside the app window on macOS, Windows & Linux 🖥️ (experimental · desktop)
  • Dedicated radio player for radio="true" streams 📻

Live TV & EPG

  • EPG / XMLTV TV guide with a live timeline ribbon and multi-channel grid (desktop)
  • TV archive / catch-up / timeshift (desktop)
  • Group-based channel list, channel-number selection, and search 🔍

Movies & series (VOD)

  • Redesigned two-state detail pages (browse ↔ watch) with season tabs and resume positions
  • Download manager for offline movies & episodes ⬇️ (desktop)
  • "Recently added" feeds and category grids with sorting & pagination

Discovery & metadata

  • Global search across live TV, movies, and series (desktop)
  • TMDB enrichment (opt-in) — plots, cast & crew, trailers, ratings, artwork, a "Similar" rail, clickable actor pages, and a trending dashboard rail (trending rail: desktop)
  • Dashboard with recently watched & continue-watching

Organization

  • Per-playlist and global favorites, aggregated across all playlists ⭐
  • Recently viewed / watch history
  • Command palette (Ctrl/Cmd+K)

Platform

  • Cross-platform desktop (Electron) and installable PWA
  • Desktop auto-updater and mobile remote control (desktop)
  • Docker self-hosting for the PWA + web backend
  • 19 languages (translation files), light & dark themes, and keyboard shortcuts

Keyboard shortcuts

Press ? or Shift+/ in the workspace to open the in-app shortcuts list.

Area Shortcut Action
Global Ctrl/Cmd+K Open command palette
Global Ctrl/Cmd+F Open global search in the desktop app
Global Ctrl/Cmd+R Open recently viewed in the desktop app
Global Enter in workspace search Submit the current search
Global F11 Toggle app window fullscreen in the desktop app
Navigation Ctrl/Cmd+B Toggle the live sidebar
Navigation 0-9 Select an M3U channel by number
Playback Space / K Play or pause playback
Playback F Toggle player fullscreen
Playback ArrowLeft / ArrowRight Seek VOD playback by 5 seconds
Playback ArrowUp / ArrowDown Adjust volume by 5%
Playback M Mute audio
Dialogs and lists ArrowUp / ArrowDown Move command palette selection
Dialogs and lists Enter Run the selected command or open a focused item
Dialogs and lists Escape Close dialogs and dismiss overlays

The desktop app can also open at its last size, maximized, or fullscreen on every launch (Settings → General → "Window on startup"), and iptvnator --fullscreen forces a single fullscreen launch for TV or HTPC autostart scripts without changing that setting.

Screenshots:

Dashboard with recently watched content Live channels with inline player and EPG
Dashboard with recently watched content Live channels with inline player and EPG
Add playlist dialog for M3U, Xtream, and Stalker Live category channel list
Add playlist dialog for M3U, Xtream, and Stalker Live category channel list
Global search across live TV, movies, and series Manage visible live categories
Global search across live TV, movies, and series Manage visible live categories
Movie category grid with sorting and pagination Recently added movies and series
Movie category grid with sorting and pagination Recently added movies and series
VOD details with playback and download actions Download manager
VOD details with playback and download actions Download manager
Multi-channel EPG grid External MPV player support
Multi-channel EPG grid External MPV player support
Radio playback with dedicated audio player Light theme
Radio playback with dedicated audio player Light theme
Application settings
Application settings

Note: First version of the application which was developed as a PWA is available in an extra git branch.

Self-hosted PWA

The Docker setup builds the Angular PWA and the monorepo web backend into one image. The backend handles remote M3U parsing plus Xtream and Stalker proxy requests under /api, so a separate 4gray/iptvnator-backend container is not required for the default self-hosted flow.

docker compose -f docker/docker-compose.yml up --build -d

The application is available at http://localhost:4333. See docker/docker-compose.yml for the ready-to-run compose file and docker/README.md for environment variables, reverse proxy notes, PWA limitations, and build details.

The self-hosted image runs the browser PWA rather than the Electron desktop app: EPG/XMLTV panels, Embedded MPV, managed MPV/VLC launching, the download manager, and Electron remote-control features are not available there. If browser playback fails, copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA.

Download

Download the latest version of the application for macOS, Windows, and Linux from the release page.

Alternatively, you can install the application using one of the following package managers:

Homebrew

$ brew install iptvnator

Snap

$ sudo snap install iptvnator

Arch

Also available as an Arch PKG, iptvnator-bin, in the AUR (using your favourite AUR-helper, .e.g. yay)

$ yay -S iptvnator-bin

Gentoo

You can install IPTVnator from the gentoo-zh overlay

sudo eselect repository enable gentoo-zh
sudo emerge --sync gentoo-zh
sudo emerge iptvnator-bin

Linux Embedded MPV Support

Embedded MPV on Linux is experimental and currently supports x64 desktop sessions where IPTVnator runs under X11 or Xwayland. Native Wayland embedding is not supported yet. Linux package launchers request X11 with --ozone-platform=x11, so Wayland desktops still need Xwayland available.

The Linux backend starts a system mpv executable with --wid, so mpv must be installed and available on PATH. CI validates the Linux native addon and standard packages on Ubuntu 22.04, with Flatpak packaging built on Ubuntu 24.04. Expected user targets are Ubuntu/Debian .deb, Arch/Manjaro pacman, RPM distributions, and AppImage on x64 systems with X11/Xwayland plus mpv installed. Flatpak and Snap builds remain available, but embedded MPV is not announced as supported there yet because those sandboxed formats do not expose the host mpv executable to the embedded backend by default.

Get it from the Snap Store

Sponsor on GitHub Support on Ko-fi

Troubleshooting

macOS: "App is damaged and can't be opened"

Older unsigned macOS builds may require removing the quarantine flag from the downloaded application:

xattr -c /Applications/IPTVnator.app

Alternatively, if the app is located in a different directory:

xattr -c ~/Downloads/IPTVnator.app

Linux: chrome-sandbox Issues

If you encounter the following error when launching IPTVnator:

The SUID sandbox helper binary was found, but is not configured correctly.
Rather than run without sandboxing I'm aborting now.
You need to make sure that chrome-sandbox is owned by root and has mode 4755.

Solution 1: Fix chrome-sandbox permissions (Recommended for .deb/.rpm installations)

Navigate to the IPTVnator installation directory and run:

sudo chown root:root chrome-sandbox
sudo chmod 4755 chrome-sandbox

Solution 2: Launch with --no-sandbox flag

Edit the desktop launcher file to add the --no-sandbox flag:

  1. Find your desktop file location:

    • Ubuntu/Debian: ~/.local/share/applications/iptvnator.desktop
    • System-wide: /usr/share/applications/iptvnator.desktop
  2. Edit the file and modify the Exec line:

    Exec=iptvnator --no-sandbox %U
    
  3. Save the file and relaunch the application from your application menu.

Alternatively, you can launch IPTVnator from the terminal with the flag:

iptvnator --no-sandbox

GNU/Linux: Wayland startup failure

If IPTVnator exits on GNU/Linux with errors about failing to connect to Wayland or initialize the Ozone platform, force X11/XWayland instead:

iptvnator --ozone-platform=x11

This workaround is mainly for older or problematic Linux graphics stacks. The Snap package already includes this X11 override by default. For AppImage, direct binaries, and other Linux package formats, pass the flag manually when needed.

How to Build and Develop

Requirements:

  • Node.js 22.13–22.x or 24 and newer with pnpm (via Corepack)
  1. Clone this repository and install project dependencies:

    $ corepack enable
    $ pnpm install
    
  2. Start the application:

    $ pnpm run serve:backend
    

This will open the Electron app in a separate window, while the Angular dev server will run at http://localhost:4200.

The equivalent Nx command is:

$ nx serve electron-backend

To start Electron with an empty, isolated data directory instead of your normal ~/.iptvnator folder, set IPTVNATOR_E2E_DATA_DIR for that run:

$ rm -rf .tmp/iptvnator-empty && mkdir -p .tmp/iptvnator-empty
$ IPTVNATOR_E2E_DATA_DIR="$PWD/.tmp/iptvnator-empty" pnpm run serve:backend

This redirects the SQLite database, Electron user data, and local config under the given directory. Delete that directory whenever you want a fresh empty state.

If you need startup diagnostics for a white screen or a frozen route, you can also turn on opt-in Electron tracing. These logs are written to the Electron terminal output so they still help when the renderer DevTools never open:

$ IPTVNATOR_TRACE_STARTUP=1 pnpm run serve:backend

Nx equivalent:

$ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend

Useful narrower flags:

  • IPTVNATOR_TRACE_IPC=1 logs renderer window.electron.* calls reaching the Electron bridge
  • IPTVNATOR_TRACE_DB=1 logs DB worker requests and request-scoped DB events
  • IPTVNATOR_TRACE_SQL=1 logs SQLite statements in both the main connection and DB worker connection
  • IPTVNATOR_TRACE_WINDOW=1 logs BrowserWindow load, navigation, and unresponsive events
  • IPTVNATOR_TRACE_RENDERER_CONSOLE=1 mirrors renderer console messages into the Electron terminal output

Security-sensitive network compatibility flags are opt-in:

  • IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS=1 permits strict EPG fetches from playlist metadata (x-tvg-url, url-tvg, or tvg-url) to resolve to localhost, LAN, or other private addresses. Directly configured Xtream/Stalker portals and private playlist servers remain supported without this flag. Prefer the in-app source-scoped “Allow source” action for a trusted EPG URL.
  • IPTVNATOR_ALLOW_INSECURE_TLS=1 disables certificate validation for remote playlist imports and refreshes for the whole Electron process. Prefer the in-app host-scoped trust action for a trusted provider with a self-signed or otherwise invalid certificate.

If the local Nx daemon gets into a bad state before rerunning Electron, reset it:

$ pnpm nx reset

To run only the Angular app without Electron, use:

$ pnpm run serve:frontend

Disclaimer

IPTVnator doesn't provide any playlists or other digital content.

Trademark

The name "IPTVnator" and the IPTVnator logo are unregistered trademarks of the project owner. The MIT license covers the source code only — it does not grant rights to the name or logo. Forks and redistributions (including app-store submissions) must use a different name and their own icon. See TRADEMARK.md for details.

All Contributors

Languages
TypeScript 81.4%
JavaScript 8.9%
HTML 3.3%
SCSS 2.8%
Astro 1.3%
Other 2.1%