diff --git a/CLAUDE.md b/CLAUDE.md
index a061c5748..1ddc0c668 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -330,7 +330,7 @@ This is an Nx monorepo with the following structure:
- **apps/electron-backend-e2e** - Playwright E2E tests against the Electron app
- **apps/stalker-mock-server** - Mock Stalker/Ministra portal for dev and E2E
- **apps/xtream-mock-server** - Mock Xtream Codes API for dev and E2E
-- **apps/website** - Astro + Tailwind landing page, blog (guides carry `faq:` frontmatter → FAQPage JSON-LD), per-OS download landing pages (`/download/`, `/download/{windows,macos,linux}/`) and feature landing pages (`/features/`, registry in `src/lib/features.ts`); direct asset links are resolved at build time from the GitHub Releases API with a `package.json` fallback (`src/lib/downloads.ts`, see `apps/website/README.md`)
+- **apps/website** - Astro + Tailwind landing page, blog (guides carry `faq:` frontmatter → FAQPage JSON-LD), per-OS download landing pages (`/download/`, `/download/{windows,macos,linux}/`) feature landing pages (`/features/`, registry in `src/lib/features.ts`) and comparison pages (`/compare/`, registry in `src/lib/comparisons.ts`, comparing IPTVnator's own options rather than other products); direct asset links are resolved at build time from the GitHub Releases API with a `package.json` fallback (`src/lib/downloads.ts`, see `apps/website/README.md`)
- **libs/** - Shared libraries:
- **epg/data-access** - EPG services, runtime bridge, program normalization
- **m3u-state** - NgRx state management for M3U playlists
diff --git a/apps/website/README.md b/apps/website/README.md
index a6629739e..5e53112b3 100644
--- a/apps/website/README.md
+++ b/apps/website/README.md
@@ -96,3 +96,22 @@ per-page switcher, the homepage feature cards and
registry entry and one `.astro` file. Screenshots come only from the
mock-backed guide and release captures, never from the older homepage
screenshots that show real channel names.
+
+## Comparison Pages
+
+`/compare/` plus one page per decision the app asks users to make
+(`m3u-vs-xtream-vs-stalker`, `playback-engines`, `desktop-vs-browser`) live in
+`apps/website/src/pages/compare/`. They compare IPTVnator's own options against
+each other, never other products, so every claim is checkable against this
+repository; the registry is `src/lib/comparisons.ts`.
+
+Each page opens with a one-paragraph verdict (`CompareHero`), carries at least
+one `ComparisonTable` (cells are `true`, `false` or a qualifying string) and
+emits `WebPage` / `FAQPage` / `BreadcrumbList` JSON-LD from
+`src/lib/comparison-schema.ts` — deliberately not `SoftwareApplication`, since
+these pages are guidance rather than a product listing, and
+`tools/testing/website-compare-pages.test.mjs` asserts that.
+
+Naming a competitor on these pages is a product decision, not a technical one.
+Phase 3 of `.plans/2026-09-03-marketing-landing-pages.md` covers that and is
+still open.
diff --git a/apps/website/project.json b/apps/website/project.json
index 8d39a04f5..1bc8356f0 100644
--- a/apps/website/project.json
+++ b/apps/website/project.json
@@ -18,7 +18,7 @@
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
- "command": "node --test tools/testing/website-giscus-comments.test.mjs tools/testing/website-download-pages.test.mjs tools/testing/website-guides.test.mjs tools/testing/website-feature-pages.test.mjs"
+ "command": "node --test tools/testing/website-giscus-comments.test.mjs tools/testing/website-download-pages.test.mjs tools/testing/website-guides.test.mjs tools/testing/website-feature-pages.test.mjs tools/testing/website-compare-pages.test.mjs"
}
},
"serve": {
diff --git a/apps/website/public/llms.txt b/apps/website/public/llms.txt
index 618888e06..9f695618a 100644
--- a/apps/website/public/llms.txt
+++ b/apps/website/public/llms.txt
@@ -20,6 +20,10 @@
- Feature, Stalker portal player: https://4gray.github.io/iptvnator/features/stalker-portal-player/
- Feature, TV guide (EPG): https://4gray.github.io/iptvnator/features/epg/
- Feature, phone remote control: https://4gray.github.io/iptvnator/features/remote-control/
+- Comparisons overview: https://4gray.github.io/iptvnator/compare/
+- Compare, M3U vs Xtream Codes vs Stalker portal: https://4gray.github.io/iptvnator/compare/m3u-vs-xtream-vs-stalker/
+- Compare, playback engines (built-in vs MPV/VLC vs embedded MPV): https://4gray.github.io/iptvnator/compare/playback-engines/
+- Compare, desktop app vs self-hosted browser version: https://4gray.github.io/iptvnator/compare/desktop-vs-browser/
- Release Notes (v0.18): https://4gray.github.io/iptvnator/blog/v0-18-release-notes/
- Troubleshooting (macOS app damaged fix): https://4gray.github.io/iptvnator/blog/macos-arm-app-damaged-fix/
- Source Code: https://github.com/4gray/iptvnator
diff --git a/apps/website/src/components/Header.astro b/apps/website/src/components/Header.astro
index a15a29edf..824d116e9 100644
--- a/apps/website/src/components/Header.astro
+++ b/apps/website/src/components/Header.astro
@@ -1,6 +1,7 @@
---
const navLinks = [
{ label: 'Features', href: '/iptvnator/features/' },
+ { label: 'Compare', href: '/iptvnator/compare/' },
{ label: 'Screenshots', href: '/iptvnator/#screenshots' },
{ label: 'Download', href: '/iptvnator/download/' },
{ label: 'Blog', href: '/iptvnator/blog/' },
diff --git a/apps/website/src/components/compare/CompareHero.astro b/apps/website/src/components/compare/CompareHero.astro
new file mode 100644
index 000000000..64d4053be
--- /dev/null
+++ b/apps/website/src/components/compare/CompareHero.astro
@@ -0,0 +1,45 @@
+---
+import type { ComparisonEntry } from '../../lib/comparisons';
+import { COMPARE_HUB_HREF } from '../../lib/comparisons';
+
+interface Props {
+ comparison: ComparisonEntry;
+ titleLead: string;
+ titleAccent: string;
+ lede: string;
+ /** The short answer, stated before the detail. */
+ verdict: string;
+}
+
+const { comparison, titleLead, titleAccent, lede, verdict } = Astro.props;
+---
+
+
+
+
+
diff --git a/apps/website/src/lib/comparison-schema.ts b/apps/website/src/lib/comparison-schema.ts
new file mode 100644
index 000000000..554558b4e
--- /dev/null
+++ b/apps/website/src/lib/comparison-schema.ts
@@ -0,0 +1,50 @@
+import type { FaqEntry } from './download-schema';
+import type { ComparisonEntry } from './comparisons';
+import { absoluteUrl } from './site';
+
+interface ComparisonPageSchemaInput {
+ comparison: ComparisonEntry;
+ pageUrl: string;
+ description: string;
+ faq: FaqEntry[];
+}
+
+/**
+ * WebPage + FAQPage + BreadcrumbList. Deliberately not SoftwareApplication:
+ * these pages are guidance about choosing between options, not a product
+ * listing, and marking them up as one would misdescribe them.
+ */
+export function buildComparisonPageSchema(input: ComparisonPageSchemaInput): Array> {
+ const { comparison, pageUrl, description, faq } = input;
+
+ return [
+ {
+ '@context': 'https://schema.org',
+ '@type': 'WebPage',
+ name: comparison.label,
+ description,
+ url: pageUrl,
+ inLanguage: 'en',
+ isPartOf: { '@type': 'WebSite', name: 'IPTVnator', url: absoluteUrl('/') },
+ about: { '@type': 'SoftwareApplication', name: 'IPTVnator', applicationCategory: 'MultimediaApplication' },
+ },
+ {
+ '@context': 'https://schema.org',
+ '@type': 'FAQPage',
+ mainEntity: faq.map((entry) => ({
+ '@type': 'Question',
+ name: entry.q,
+ acceptedAnswer: { '@type': 'Answer', text: entry.a },
+ })),
+ },
+ {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: [
+ { '@type': 'ListItem', position: 1, name: 'IPTVnator', item: absoluteUrl('/') },
+ { '@type': 'ListItem', position: 2, name: 'Compare', item: absoluteUrl('/compare/') },
+ { '@type': 'ListItem', position: 3, name: comparison.label, item: pageUrl },
+ ],
+ },
+ ];
+}
diff --git a/apps/website/src/lib/comparisons.ts b/apps/website/src/lib/comparisons.ts
new file mode 100644
index 000000000..b2ceb092d
--- /dev/null
+++ b/apps/website/src/lib/comparisons.ts
@@ -0,0 +1,65 @@
+/**
+ * Registry of the comparison pages under `/compare/`. These pages compare
+ * IPTVnator's own options against each other — connection types, playback
+ * engines, editions — rather than naming other products, so every claim on
+ * them can be checked against this repository.
+ */
+
+export type ComparisonSlug =
+ | 'm3u-vs-xtream-vs-stalker'
+ | 'playback-engines'
+ | 'desktop-vs-browser';
+
+export interface ComparisonEntry {
+ slug: ComparisonSlug;
+ /** Short name for breadcrumbs, cards and the switcher. */
+ label: string;
+ /** Mono eyebrow above the page title. */
+ eyebrow: string;
+ /** The question the page answers, one sentence. */
+ question: string;
+ /** Site-relative href including the GitHub Pages base. */
+ href: string;
+ /** 24×24 stroke icon path. */
+ icon: string;
+}
+
+export const COMPARISONS: readonly ComparisonEntry[] = [
+ {
+ slug: 'm3u-vs-xtream-vs-stalker',
+ label: 'M3U vs Xtream Codes vs Stalker',
+ eyebrow: 'Compare · Connection types',
+ question:
+ 'Your provider gave you a link, a login or a MAC address. Which one should you use, and what does each give you inside the app?',
+ href: '/iptvnator/compare/m3u-vs-xtream-vs-stalker/',
+ icon: 'M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z',
+ },
+ {
+ slug: 'playback-engines',
+ label: 'Playback engines',
+ eyebrow: 'Compare · Players',
+ question:
+ 'Built-in web players, MPV, VLC or the embedded MPV engine: which one plays your streams, and what do you give up by switching?',
+ href: '/iptvnator/compare/playback-engines/',
+ icon: 'M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
+ },
+ {
+ slug: 'desktop-vs-browser',
+ label: 'Desktop app vs browser version',
+ eyebrow: 'Compare · Editions',
+ question:
+ 'The desktop app and the self-hosted browser version share one codebase. Here is exactly what the browser cannot do.',
+ href: '/iptvnator/compare/desktop-vs-browser/',
+ icon: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
+ },
+];
+
+export const COMPARE_HUB_HREF = '/iptvnator/compare/';
+
+export function comparisonBySlug(slug: ComparisonSlug): ComparisonEntry {
+ const entry = COMPARISONS.find((comparison) => comparison.slug === slug);
+ if (!entry) {
+ throw new Error(`Unknown comparison slug: ${slug}`);
+ }
+ return entry;
+}
diff --git a/apps/website/src/pages/compare/desktop-vs-browser.astro b/apps/website/src/pages/compare/desktop-vs-browser.astro
new file mode 100644
index 000000000..f33654276
--- /dev/null
+++ b/apps/website/src/pages/compare/desktop-vs-browser.astro
@@ -0,0 +1,157 @@
+---
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import FaqAccordion from '../../components/blog/FaqAccordion.astro';
+import CopyCommand from '../../components/blog/CopyCommand.astro';
+import DownloadSection from '../../components/download/DownloadSection.astro';
+import FeatureList from '../../components/download/FeatureList.astro';
+import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro';
+import RelatedPosts from '../../components/download/RelatedPosts.astro';
+import CompareHero from '../../components/compare/CompareHero.astro';
+import ComparisonTable from '../../components/compare/ComparisonTable.astro';
+import CompareSwitcher from '../../components/compare/CompareSwitcher.astro';
+import type { FaqEntry } from '../../lib/download-schema';
+import { buildComparisonPageSchema } from '../../lib/comparison-schema';
+import { comparisonBySlug } from '../../lib/comparisons';
+import { REPO_URL } from '../../lib/downloads';
+import { absoluteUrl } from '../../lib/site';
+
+const comparison = comparisonBySlug('desktop-vs-browser');
+const pageUrl = absoluteUrl('/compare/desktop-vs-browser/');
+const title = 'IPTVnator Desktop App vs Self-Hosted Browser Version – What Differs';
+const description =
+ 'IPTVnator ships as a desktop app for Windows, macOS and Linux and as a self-hosted browser version you run from one Docker container. Both play M3U, Xtream Codes and Stalker sources; the browser version leaves out external players, downloads, the XMLTV guide and the phone remote.';
+
+const columns = ['Desktop app', 'Browser version'];
+
+const rows = [
+ { label: 'M3U and M3U8 playlists', cells: [true, true] },
+ { label: 'Xtream Codes accounts', cells: [true, true] },
+ { label: 'Stalker and Ministra portals', cells: [true, true] },
+ { label: 'Program guide from a portal', cells: [true, true], note: 'Xtream and Stalker publish their own schedule.' },
+ { label: 'XMLTV guide files', cells: [true, false], note: 'Downloading, parsing and storing XMLTV needs the desktop runtime.' },
+ { label: 'Manual EPG channel mapping', cells: [true, false] },
+ { label: 'MPV, VLC and embedded MPV', cells: [true, false], note: 'A browser cannot launch another application; copy the stream URL instead.' },
+ { label: 'Offline downloads and recordings', cells: [true, false] },
+ { label: 'Phone remote control', cells: [true, false], note: 'The desktop app is what serves the remote page.' },
+ { label: 'Search across every source', cells: [true, false] },
+ { label: 'Local catalog for instant opening', cells: ['Local database', 'Browser storage'] },
+ { label: 'Opening a playlist file from the system', cells: [true, false] },
+ { label: 'Reachable from other devices', cells: [false, true], note: 'This is the one thing the browser version does better.' },
+ { label: 'Installation', cells: ['Installer or package', 'One container you run yourself'] },
+];
+
+const browserFor = [
+ {
+ title: 'A machine you cannot install on',
+ description:
+ 'A locked-down work laptop, a shared computer, a tablet. The browser version needs nothing installed locally beyond a browser.',
+ },
+ {
+ title: 'A home server',
+ description:
+ 'Run the container once and reach the same library from every device on your network. The container includes the backend that fetches playlists and proxies portal requests.',
+ },
+ {
+ title: 'Trying it before installing',
+ description:
+ 'The browser version is the same application, so what you see there is what the desktop app looks like, minus the features that need a desktop.',
+ },
+];
+
+const faq: FaqEntry[] = [
+ {
+ q: 'Is the browser version a different app?',
+ a: 'No, it is the same application built for the browser. The desktop app wraps it and adds what only a desktop can do: launching MPV or VLC, downloading files, storing an XMLTV guide, serving the phone remote and registering playlist file types.',
+ },
+ {
+ q: 'Why can the browser version not show an XMLTV guide?',
+ a: 'A guide file is downloaded, parsed and stored on the device, which the desktop runtime does in a background worker and a local database. Portal schedules from Xtream and Stalker sources do work in the browser, because they come from the provider on demand.',
+ },
+ {
+ q: 'How do I run the browser version?',
+ a: 'With the Docker setup in the repository. It builds the web app and its backend into one image and serves them together; the compose file and the environment variables are documented in the docker directory.',
+ },
+ {
+ q: 'Can I use both at once?',
+ a: 'Yes, but they do not share data. The desktop app keeps a local database on your machine and the browser version keeps its data in that browser, so sources, favorites and history exist separately in each.',
+ },
+ {
+ q: 'Is the browser version safe to expose to the internet?',
+ a: 'Treat it as a private service. It has no user accounts, and its backend proxies requests to your providers. Run it on your own network or behind your own authenticating reverse proxy.',
+ },
+ {
+ q: 'Which one should I install?',
+ a: 'The desktop app, unless you specifically need access from other devices or cannot install software. It is the complete version and it is what most of the documentation assumes.',
+ },
+];
+
+const jsonLd = buildComparisonPageSchema({ comparison, pageUrl, description, faq });
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The application is then available on port 4333. Environment variables, reverse-proxy notes and the
+ limitations above are documented in{' '}
+
+ the docker directory
+ .
+
+
+
+
+ If inline playback fails in the browser, there is no external player to fall back to. Copy the stream URL and
+ open it in MPV, VLC or IINA yourself; the desktop app is the version that automates that step.
+
+
+ Data lives where the app runs: the desktop app in a local database, the browser version in that browser's own
+ storage. Moving between them means adding the sources again.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/website/src/pages/compare/index.astro b/apps/website/src/pages/compare/index.astro
new file mode 100644
index 000000000..062cec2d5
--- /dev/null
+++ b/apps/website/src/pages/compare/index.astro
@@ -0,0 +1,105 @@
+---
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import { COMPARISONS } from '../../lib/comparisons';
+import { FEATURES } from '../../lib/features';
+import { absoluteUrl } from '../../lib/site';
+
+const pageUrl = absoluteUrl('/compare/');
+const title = 'Compare Your Options in IPTVnator – Sources, Players and Editions';
+const description =
+ 'Short, checkable comparisons for the choices IPTVnator asks you to make: which connection type to use for your provider, which playback engine plays your streams, and what the self-hosted browser version leaves out.';
+
+const jsonLd = [
+ {
+ '@context': 'https://schema.org',
+ '@type': 'CollectionPage',
+ name: 'Compare your options in IPTVnator',
+ description,
+ url: pageUrl,
+ inLanguage: 'en',
+ isPartOf: { '@type': 'WebSite', name: 'IPTVnator', url: absoluteUrl('/') },
+ hasPart: COMPARISONS.map((comparison) => ({
+ '@type': 'WebPage',
+ name: comparison.label,
+ url: absoluteUrl(comparison.href.replace('/iptvnator', '')),
+ })),
+ },
+ {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: [
+ { '@type': 'ListItem', position: 1, name: 'IPTVnator', item: absoluteUrl('/') },
+ { '@type': 'ListItem', position: 2, name: 'Compare', item: pageUrl },
+ ],
+ },
+];
+---
+
+
+
+
+
+
+
+ Compare
+
+ Which option is right for you
+
+
+ IPTVnator asks you to choose a few times: how to connect your provider, which engine plays a stream, and which
+ edition to install. Each page below answers one of those with a short verdict and a table you can check.
+
+
+
diff --git a/apps/website/src/pages/compare/m3u-vs-xtream-vs-stalker.astro b/apps/website/src/pages/compare/m3u-vs-xtream-vs-stalker.astro
new file mode 100644
index 000000000..11e5ff281
--- /dev/null
+++ b/apps/website/src/pages/compare/m3u-vs-xtream-vs-stalker.astro
@@ -0,0 +1,152 @@
+---
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import FaqAccordion from '../../components/blog/FaqAccordion.astro';
+import DownloadSection from '../../components/download/DownloadSection.astro';
+import FeatureList from '../../components/download/FeatureList.astro';
+import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro';
+import RelatedPosts from '../../components/download/RelatedPosts.astro';
+import CompareHero from '../../components/compare/CompareHero.astro';
+import ComparisonTable from '../../components/compare/ComparisonTable.astro';
+import CompareSwitcher from '../../components/compare/CompareSwitcher.astro';
+import type { FaqEntry } from '../../lib/download-schema';
+import { buildComparisonPageSchema } from '../../lib/comparison-schema';
+import { comparisonBySlug } from '../../lib/comparisons';
+import { absoluteUrl } from '../../lib/site';
+
+const comparison = comparisonBySlug('m3u-vs-xtream-vs-stalker');
+const pageUrl = absoluteUrl('/compare/m3u-vs-xtream-vs-stalker/');
+const title = 'M3U vs Xtream Codes vs Stalker Portal – Which One Should You Use?';
+const description =
+ 'The three ways IPTV providers hand out access, compared: an M3U playlist link, an Xtream Codes login, and a Stalker or Ministra portal with a MAC address. What each one gives you inside IPTVnator, how to tell which one you were given, and when the same account works both ways.';
+
+const columns = ['M3U playlist', 'Xtream Codes', 'Stalker portal'];
+
+const rows = [
+ { label: 'What you enter', cells: ['A link, a file or pasted text', 'Server URL, username, password', 'Portal URL and MAC address'] },
+ { label: 'Live TV channels', cells: [true, true, true] },
+ { label: 'Movies and series as a catalog', cells: ['Only as flat entries', true, true], note: 'A playlist can list movie files, but it carries no seasons, categories or detail pages.' },
+ { label: 'Posters, descriptions, cast', cells: ['From TMDB only', true, true] },
+ { label: 'Program guide', cells: ['XMLTV you supply', 'From the provider', 'From the portal'] },
+ { label: 'Catch-up / archive', cells: ['If the playlist declares it', true, true] },
+ { label: 'Resume where you stopped', cells: ['Movies only', true, true] },
+ { label: 'Offline downloads', cells: [false, true, true], note: 'Desktop app only, for movies and episodes.' },
+ { label: 'Local catalog for instant opening', cells: ['Whole playlist is stored', true, 'Lists are fetched on demand'] },
+ { label: 'Account and expiry information', cells: [false, true, true] },
+ { label: 'Refresh from the provider', cells: [true, true, 'Continuous'] },
+ { label: 'Works in the browser version', cells: [true, true, true], note: 'The self-hosted web version proxies all three; the desktop-only features above stay unavailable there.' },
+];
+
+const identify = [
+ {
+ title: 'A link ending in .m3u or .m3u8',
+ description:
+ 'A plain playlist. Add it as an M3U URL, or download it and import the file. This is the lowest common denominator: channels and nothing else.',
+ },
+ {
+ title: 'A link containing get.php or player_api.php',
+ description:
+ 'An Xtream Codes account in disguise. The part before the endpoint is the server URL, and the username and password sit in the query string. Add it as Xtream credentials instead of as a playlist.',
+ },
+ {
+ title: 'A host, a username and a password',
+ description:
+ 'An Xtream Codes account. Enter the three fields and use the connection test before adding it.',
+ },
+ {
+ title: 'A portal address and a MAC address',
+ description:
+ 'A Stalker or Ministra portal, the kind MAG set-top boxes use. The MAC address is the account; IPTVnator authenticates with it the way a box does.',
+ },
+];
+
+const faq: FaqEntry[] = [
+ {
+ q: 'Which one should I pick if my provider offers several?',
+ a: 'Prefer Xtream Codes when you have a username and password: it carries movies, series, provider metadata, the program guide and catch-up that a flat playlist cannot. Use a Stalker portal when the provider identifies you by MAC address. Use a plain M3U playlist when that is all you were given, or when you keep a hand-made channel list.',
+ },
+ {
+ q: 'My M3U link contains get.php. Is that an Xtream account?',
+ a: 'Almost always, yes. Panels generate that link from an Xtream Codes account, so the same credentials work in the Xtream form and unlock the movie and series catalog. Paste the whole link into the Server URL field and IPTVnator fills in the username and password for you.',
+ },
+ {
+ q: 'Can I add all three kinds of sources at once?',
+ a: 'Yes. Every source is separate, with its own favorites, history and catalog, and the dashboard, global favorites, recent items and search combine them. Several accounts of the same kind are fine too.',
+ },
+ {
+ q: 'Which one has the best program guide?',
+ a: 'Xtream and Stalker portals publish their own schedule, so the guide works without any setup. An M3U playlist needs an XMLTV source, which you add once in the settings, or which the playlist declares in its header. On the desktop app your own XMLTV sources can also fill gaps in a portal guide.',
+ },
+ {
+ q: 'Does IPTVnator convert a Stalker portal into an M3U playlist?',
+ a: 'No. It talks to the portal directly for every list and resolves a fresh link for every stream, which is what keeps favorites and history working when temporary links expire. There is no export of portal content into a playlist file.',
+ },
+ {
+ q: 'Where do I get any of these?',
+ a: 'From an IPTV provider you have a contract with. IPTVnator is a player: it ships without channels, playlists or accounts, and it never sells them.',
+ },
+];
+
+const jsonLd = buildComparisonPageSchema({ comparison, pageUrl, description, faq });
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The same subscription is often reachable as both a playlist and an Xtream account, because the panel builds the
+ playlist link out of the account. Adding it as Xtream credentials is strictly better: you get the same channels
+ plus movies, series, the provider guide, catch-up and an expiry date, and the desktop app keeps a local catalog so
+ the source opens instantly next time.
+
+
+ Stalker portals are the exception. A MAC address is a different identity mechanism, and a portal cannot be
+ expressed as a playlist or an Xtream login. If that is what you were given, the Stalker form is the only way in.
+
+
+ Switching later costs nothing: add the better source, check that it works, and remove the old one. Favorites and
+ history belong to the source, so they do not carry over.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/website/src/pages/compare/playback-engines.astro b/apps/website/src/pages/compare/playback-engines.astro
new file mode 100644
index 000000000..a21c2271c
--- /dev/null
+++ b/apps/website/src/pages/compare/playback-engines.astro
@@ -0,0 +1,151 @@
+---
+import BaseLayout from '../../layouts/BaseLayout.astro';
+import FaqAccordion from '../../components/blog/FaqAccordion.astro';
+import DownloadSection from '../../components/download/DownloadSection.astro';
+import FeatureList from '../../components/download/FeatureList.astro';
+import OfficialSourcesNote from '../../components/download/OfficialSourcesNote.astro';
+import RelatedPosts from '../../components/download/RelatedPosts.astro';
+import CompareHero from '../../components/compare/CompareHero.astro';
+import ComparisonTable from '../../components/compare/ComparisonTable.astro';
+import CompareSwitcher from '../../components/compare/CompareSwitcher.astro';
+import type { FaqEntry } from '../../lib/download-schema';
+import { buildComparisonPageSchema } from '../../lib/comparison-schema';
+import { comparisonBySlug } from '../../lib/comparisons';
+import { absoluteUrl } from '../../lib/site';
+
+const comparison = comparisonBySlug('playback-engines');
+const pageUrl = absoluteUrl('/compare/playback-engines/');
+const title = 'Built-in Players vs MPV vs VLC in IPTVnator – Which Engine to Use';
+const description =
+ 'IPTVnator can play a stream in three built-in web players, in an embedded mpv engine, or in MPV or VLC on your machine. What each one decodes, which in-app controls it keeps, and what you give up by switching.';
+
+const engineColumns = ['HTML5 / hls.js', 'Video.js', 'ArtPlayer'];
+
+const engineRows = [
+ { label: 'HLS and MPEG-TS streams', cells: [true, true, true] },
+ { label: 'DASH with ClearKey keys', cells: [true, 'Handed to HTML5', true], note: 'A .mpd channel always plays inline; the app switches engine for it when needed.' },
+ { label: 'Shared control bar', cells: [true, true, true], note: 'On by default. Turning it off in Settings › Playback returns each player to its own controls.' },
+ { label: 'Subtitle file, offset and styling', cells: [true, false, true] },
+ { label: 'Quality selection', cells: ['Multi-variant HLS', 'Multi-variant HLS', 'Multi-variant HLS'], note: 'Offered only when the stream really carries several renditions.' },
+ { label: 'Picture-in-picture', cells: [true, true, true] },
+ { label: 'Playback error diagnostics', cells: [true, true, true] },
+];
+
+const targetColumns = ['Built-in web players', 'Embedded MPV', 'MPV or VLC'];
+
+const targetRows = [
+ { label: 'Where the video is drawn', cells: ['In the app window', 'In the app window', 'In a separate window'] },
+ { label: 'Codec coverage', cells: ['What the browser engine decodes', 'What mpv decodes', 'What mpv or VLC decodes'] },
+ { label: 'Needs anything installed', cells: [false, 'Bundled, except on some Linux packages', 'MPV or VLC on your machine'] },
+ { label: 'App controls, subtitles and quality menu', cells: [true, 'Controls and subtitles, no quality menu', false] },
+ { label: 'Picture-in-picture', cells: [true, false, false] },
+ { label: 'Live TV recording', cells: [false, true, false], note: 'Recordings appear beside downloads in the manager.' },
+ { label: 'Failure reaches the app', cells: [true, false, false], note: 'Only the built-in players raise a diagnostic, so the alternative-source failover for movies can act on it.' },
+ { label: 'Status', cells: ['Default', 'Experimental, opt-in', 'Stable'] },
+ { label: 'Available in the browser version', cells: [true, false, false] },
+];
+
+const guidance = [
+ {
+ title: 'Start with the default',
+ description:
+ 'The built-in players cover ordinary HLS and MPEG-TS streams, keep every in-app control, and are the only engines where a failed stream can trigger the automatic switch to another copy of a movie.',
+ },
+ {
+ title: 'Reach for MPV or VLC when a stream refuses',
+ description:
+ 'Odd codecs, unusual containers and streams that stutter in a browser engine usually play immediately in MPV or VLC. IPTVnator keeps the library, the guide and the phone remote; only the video moves to another window.',
+ },
+ {
+ title: 'Try embedded MPV to keep it in the window',
+ description:
+ 'The experimental engine renders mpv inside IPTVnator, so you get mpv decoding without a second window, plus live-TV recording. It is opt-in under Settings › Playback and can be switched off again.',
+ },
+ {
+ title: 'Radio and DASH decide for themselves',
+ description:
+ 'Radio stations always use the built-in audio player, and DASH channels always play inline, whatever engine you selected. Both ignore the external player setting on purpose.',
+ },
+];
+
+const faq: FaqEntry[] = [
+ {
+ q: 'Which player should I choose?',
+ a: 'Leave the default built-in player until something refuses to play. It keeps the shared controls, subtitles, quality selection and picture-in-picture, and it is the only engine family whose failures the app can react to. Switch a stream to MPV or VLC when it does not decode.',
+ },
+ {
+ q: 'What is the difference between MPV and embedded MPV?',
+ a: 'Both use mpv to decode. MPV opens its own window and is a stable, long-standing option. Embedded MPV renders inside the IPTVnator window, keeps the app controls and can record live TV, but it is experimental and must be enabled in Settings › Playback.',
+ },
+ {
+ q: 'Do I need to install MPV or VLC myself?',
+ a: 'For the external MPV and VLC options, yes: IPTVnator launches the copy on your machine and you can point it at a specific path in the settings. The built-in players need nothing, and embedded MPV ships with the app except on the Linux packages that use your system mpv.',
+ },
+ {
+ q: 'Why do subtitles behave differently between players?',
+ a: 'Loading an external subtitle file with a timing offset and custom styling is offered where the engine can do it: the HTML5 player, ArtPlayer and embedded MPV. Video.js keeps its own subtitle handling, and MPV or VLC use theirs.',
+ },
+ {
+ q: 'Does switching the player lose my progress?',
+ a: 'No. Playback positions, favorites and history belong to the source, not the player, so a movie resumes where you stopped whichever engine plays it.',
+ },
+ {
+ q: 'Can the browser version use MPV or VLC?',
+ a: 'No. Launching another application requires the desktop app. In a browser, copy the stream URL and open it in your player manually.',
+ },
+];
+
+const jsonLd = buildComparisonPageSchema({ comparison, pageUrl, description, faq });
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/website/src/pages/features/index.astro b/apps/website/src/pages/features/index.astro
index eac0560ba..514387bd2 100644
--- a/apps/website/src/pages/features/index.astro
+++ b/apps/website/src/pages/features/index.astro
@@ -67,6 +67,12 @@ const jsonLd = [
One player for the three ways providers hand out access, with a program guide, a phone remote and the
comforts of a desktop app. Each page below goes into detail and links to the matching setup guide.