IPTVnator for Android TV / Fire TV is an experimental platform build: it runs the IPTVnator browser app (the PWA built from this repository) on Fire TV sticks and Android TV boxes, driven entirely with the remote, with no server to host: the web backend is re-implemented in Java and embedded in the APK. The Android build shares the IPTVnator name and web interface with desktop and PWA builds. Version française.
APK
├── full-screen WebView ──► http://127.0.0.1:43330/ (loopback server, NanoHTTPD)
│ ├── / IPTVnator PWA (assets/www, the Angular bundle)
│ ├── /tvshell/tv.js the remote-control layer injected into index.html
│ └── /api/* embedded backend: M3U parsing, Xtream and Stalker proxies
└── Back / Menu / media keys → tv.js ; streams the WebView cannot play → VLC (intent)
Experimental shell maintained in this repository (
tv-shell/): it embeds the IPTVnator PWA built from these sources. If you redistribute a modified build outside the project, applyTRADEMARK.md(distinct name and icon).
The Android WebView and the PWA's vertical page scrollers stop at their scroll edges. The native bridge activates the PWA rule, so browser remote emulation keeps browser scrolling behavior.
Local playlist import and backup restore use Android's document picker, without broad storage permissions. Cancellation leaves the file input unchanged; devices without a document picker show an explanatory message. The shared parsers validate the selected document. Backup export is unavailable in this runtime until a native document-save flow exists. Settings offers import only; a programmatic export cannot report success.
The temporary player-loading rollback is
localStorage.setItem('iptvnator.eagerPlayerLoading', '1'), followed by a reload:
all engines load before Angular starts. Remove the key to restore deferred loading.
Remove this rollback by release 0.27 (within two releases of 0.25).
Returning from a deep catalog position synchronizes the TV grid before restoring
card focus. Shared playback diagnostics publish iptvnator:playback-issue with only a code;
the shell uses unsupported-codec/container and decode errors to offer an external
player even when the engine does not raise a native media error. Network errors
remain excluded and duplicate diagnostics share the existing per-load prompt limit.
Parser fixtures use synthetic catalog metadata and reserved provider endpoints
before the reference parser generates expected output. Run
node --test tv-shell/tests/*.test.mjs for fixture privacy and reference-backend
redirect checks. The APK workflow
runs these checks alongside the Java and remote-control suites. Cross-origin
redirects drop authorization, cookie, proxy authorization, serial and Host headers.
The reference server also accepts a leading JSON BOM and forwards Xtream's trimmed
User-Agent as a header, excluding it from provider query parameters.
Malformed redirects and interrupted response bodies return provider errors. An HTTP
response, including a redirect, rules out an HTTP retry during protocol discovery.
The embedded backend checks compressed response lengths before accepting playlist data.
The Node reference server allows the local mock provider by default. For other
providers, set TVSHELL_PROVIDER_ORIGINS to a comma-separated list of HTTP(S)
origins such as https://provider.example. Registration and every redirect hop
must use a configured origin. The embedded Android backend has its own provider
policy and encodes spaces in registered playlist URLs without re-encoding
existing escapes. The Node server serves static files and the SPA entry point
from the configured --www directory, with symbolic links confined to that directory.
- Remote-control navigation: the D-pad moves the focus between the app's focusable elements (a JavaScript port of Android's FocusFinder), OK activates, Back closes dialogs and menus then walks the history, Menu opens the focused item's context menu, play/pause/rewind/fast-forward drive the video. Virtual lists of thousands of channels work: the next row, already rendered past the fold, is the next stop, and a held arrow moves once per drawn frame. Material checkboxes and radios keep native input focus, with the focus ring painted on their visible component even when the input itself is transparent. Up/Down can leave a native radio group; Left/Right retain native radio selection. In the source switcher, Up/Down highlight sources; horizontal arrows move the search caret and leave it at the boundary. Menu opens the highlighted source's actions through the app's keyboard handler, with a mouse context-menu fallback. Visible trailer iframes can receive remote focus and handle their own keys; hidden background embeds remain excluded. Leaving the Android activity closes the trailer dialog and unloads its player. HTTP protocol discovery requires positive refusal evidence from every connection-error cause; ambiguous failures never authorize a plaintext probe. The transparent playback-position input stays focusable, with its ring on the visible track. Bridge diagnostics log method names without arguments or exception text, so stream URLs and credentials stay out of browser-emulation logs. Structural list-item wrappers are not remote actions: dashboard cards focus their links, and a programmatic landing on a wrapper enters its actionable child.
- M3U playlists by URL, Xtream Codes portals, Stalker portals through the
embedded backend: the same route contract as
apps/web-backend, with the M3U parser ported line by line and checked against 43 fixtures generated by the real JavaScript parser (tools/gen-parser-fixtures.mjs, verified in CI). Provider JSON accepts a leading UTF-8 BOM; response-body timeouts, decoding failures and truncated plain responses with a declared length are transport errors even after HTTP 200 headers arrive. - Playback with the PWA's own players (HLS
.m3u8, MP4, DASH ClearKey) in the WebView. Streams a browser cannot play (raw.ts, AC3 audio...) trigger a "This stream cannot be played here" prompt offering to open them in another player (VLC, Kodi, MX Player...). The app's "Copy stream URL" button does the same on a TV. - IPTVnator settings (long-press Menu, or hold Back on remotes without a Menu key): interface size (960 / 1280 / 1600 px), preferred external player, data reset, diagnostics (WebView version, screen).
- Data (playlists, favourites, positions) is stored by the PWA in the WebView
(IndexedDB / localStorage) and survives restarts and updates. The embedded
backend has no server storage: it answers the PWA's
/api/v1/instanceprobe with 404, the answer of a backend without it.
Inherited PWA limits: no EPG/XMLTV, no downloads, no embedded MPV (desktop-only features in IPTVnator).
- The embedded server listens on
127.0.0.1only (TvShellServer): nothing is exposed on the LAN./apirequest bodies are capped at 16 KiB (413), the same bound as the upstream web backend, and provider URLs must behttp/https. - Provider requests follow at most five redirects;
Authorization,Cookie,Proxy-Authorization,SNandHostare dropped when a redirect leaves the origin, exactly like the web backend'sValidatedHttpClient. - The WebView has file and content access off; the only top-level navigations
that leave the shell are
http(s)links handed to the system (ACTION_VIEW), any other scheme is ignored. AndroidX WebKit injectswindow.TvShell.postMessageonly at the current loopback origin. The shell handles commands from the main frame of that origin. WebViews without this feature show an update prompt. - The published APK is a non-debuggable release build,
allowBackupis off (the WebView storage holds portal credentials) and remote debugging (chrome://inspect) is opt-in in the IPTVnator settings. - Native diagnostics and forwarded WebView errors share a redactor. It removes full URLs, including signed paths containing apostrophes, plus credential fields and authorization values before logging or displaying startup diagnostics. Other console levels are consumed without forwarding their original text; each diagnostic is capped at 4,000 characters.
- Cleartext traffic is allowed on purpose: the app's own origin is
http://127.0.0.1and most IPTV portals and streams are plainhttp. - Known limitation: any other app on the device can reach the loopback server and make it fetch provider URLs. There is no secret between the WebView and the server yet.
The Build TV shell APK workflow runs on pull requests and master pushes that
touch tv-shell/ (artifact only) and on demand (Actions → Build TV shell APK
→ Run workflow; tick create_release to also publish a GitHub release
tv-shell-v0.1.0-N). The release is a pre-release never marked "Latest"
unless you untick prerelease: keep it on in the IPTVnator repository itself,
where a Latest release without the desktop updater's files would break
auto-update; a fork publishing for its own users can untick it. It builds the PWA
(pnpm nx build web --configuration=pwa, as the Docker image does), checks the
parser fixtures against the installed parser, drives that PWA with the two
remote-control suites in Chromium (screenshots are uploaded when one fails),
runs the backend tests, assembles the release APK and uploads
iptvnator-tv-shell-0.1.0-<sha>.apk.
Signing: without secrets the APK is signed with the versioned debug key
(android/app/debug.keystore, password android), which is conventional, not
secret, and stable across builds, so newer builds install over older ones. Add
the TVSHELL_KEYSTORE_B64 (base64 keystore), TVSHELL_KEYSTORE_PASSWORD,
TVSHELL_KEY_ALIAS and TVSHELL_KEY_PASSWORD secrets for a release key. The
versionCode is the commit time in minutes, so every build upgrades the
previous one.
The Android startup mark, legacy and adaptive launcher icons, and TV banner use
the shared IPTVnator artwork. Regenerate them with
node tv-shell/tools/gen-brand-assets.mjs.
# 1. the PWA, from the repository root
pnpm install --frozen-lockfile --ignore-scripts
pnpm nx build web --configuration=pwa
# 2. copy it into the shell
tv-shell/scripts/bundle-pwa.sh dist/apps/web
# 3. the APK (JDK 17 + Android SDK; Android Studio provides both)
cd tv-shell/android && ./gradlew :backend:check :app:assembleRelease
# → app/build/outputs/apk/release/app-release.apk (signed with the debug key)- Settings → My Fire TV → Developer options: enable Apps from unknown sources (or Install unknown apps → Downloader) and, for ADB, ADB debugging.
- Downloader: install the Downloader app from the Appstore, enter the
APK's URL from the GitHub release (right-click the
.apkasset → copy link), install. - ADB (from a PC on the same network):
adb connect 192.168.x.x # the stick's IP: Settings → My Fire TV → About → Network adb install -r iptvnator-tv-shell-0.1.0-xxxxxxx.apk - Start IPTVnator. Add a source: Sources → Add M3U / Add Xtream / Add Stalker; OK on a text field opens the Fire TV keyboard.
- Install VLC for Android (Appstore) for
.tsstreams.
| Remote | Action |
|---|---|
| D-pad | move between elements (landing on a text field does not open the keyboard); on the picture: ◀ ▶ seek, ▲ ▼ leave the picture / reach the controls; in a text field the arrows move the caret until it cannot move further; in the source switcher's search: ▲ ▼ highlight a source without leaving the field |
| OK | activate; on the picture: fullscreen; on a text field: keyboard. A held OK is one activation |
| Back | close dialog/menu → leave fullscreen → previous page → (twice) exit the app; hold: IPTVnator settings (remotes without a Menu key) |
| Menu | context menu of the focused item; on the picture: open the stream in VLC; long press: IPTVnator settings. The HDMI-CEC context-menu key and Y on a game controller do the same |
| ▶⏸ ⏪ ⏩ | play/pause, −15 s, +30 s |
A physical keyboard can edit a D-pad-focused text field directly. Left and Right adjust the focused timeline or volume slider in fullscreen. Right from a channel goes through its available row actions on the way to the preview.
- No LAN server: the backend is embedded in Java (
WebBackend) with the same contract asapps/web-backend(provider-targets / parse / xtream / stalker)./parse-xmldeliberately answers 501: the PWA does not show the EPG. - Protocol discovery follows the Xtream contract. Only an initial refused connection or positively identified TLS wrong-version failure can permit the explicit HTTP probe. Any HTTP response in the redirect chain blocks it; certificate errors and mixed or incomplete TLS evidence never authorize a downgrade. The response exposes only credential-free failure metadata.
- Fixed origin
http://127.0.0.1:43330: the PWA's storage belongs to the origin, so the port of the first start is remembered and never replaced. When another app holds it, the shell retries it briefly, then serves on another port (43330 to 43334) and says so in a toast and in the diagnostics. Each return to the app tries the saved port again and switches back to the user's data as soon as it is free. - The Angular service worker is not served (deliberate 404): the bundle ships inside the APK, caching would only make updates stale.
tv.jsis loaded before the bundle through a same-origin<script src="tvshell/tv.js">tag (the app's CSP forbids inline scripts). No IPTVnator file is modified. It sendsonReadyto the native bridge once Angular has rendered; the shell shows the error panel with the diagnostics when that does not happen within 30 s or when the page reports anUncaught SyntaxError. The 30 s wait doubles once while the page is still being fetched (a cold start after an update), starts again when the bundle has run (Angular's bootstrap gets its own period), and pauses while another app is in front (the page's timers are paused then). A main-frame load error from the loopback server is retried once after the server is restarted, an HTTP error from it opens the panel at once. The splash hides as soon as the web app reports readiness. While the panel is up, the remote's keys always land on its buttons.- The WebView is created in code, not inflated: when the system WebView is missing, disabled or being updated, the error panel says so instead of the app crashing at start.
- The screen is kept on only while a
<video>/<audio>plays (and during the splash):tv.jsreportsplaying/pause/ended/emptiedto the shell, so the screensaver and the device's sleep work on lists and menus. - After Android closes the app, or only its renderer, in the background, it reopens on the route it was showing (saved instance state; the server's SPA fallback serves any route). A display change that alters the scale reloads the same route.
- Layout width 1280 CSS px (960 / 1600 selectable) with the page scale pinned
from the measured screen width (
DisplayMetrics; a 1080p Fire TV is 960 CSS px wide, so the scale is 0.75): the WebView's own overview-mode fit proved unreliable on the device. - The WebView renderer may be killed on a 1 GB device:
onRenderProcessGonerecreates the Activity (the server survives in the Application); three crashes, or deaths while the app is in front, within a minute show the error panel instead of looping. The system reclaiming the hidden renderer while VLC or another app is in front does not count. - Memory on a 1 GB device: provider answers are capped at an eighth of the app's
heap (64 MB at most, about 32 MB on a Fire TV Stick Lite) and refused before
download when the declared size is larger (
ERR_RESPONSE_TOO_LARGE). Xtream and Stalker JSON answers are validated and passed through verbatim instead of being rebuilt as maps; responses are encoded straight to UTF-8 in chunks (no doubling buffer, no final copy) and served from them. An M3U import is parsed and written in one pass: the parser walks the text without splitting it into a second copy and hands over each channel as soon as it is complete, which/api/parsedecorates (id, DRM) and writes out at once, so the heap holds the playlist text and the JSON answer, never the channel list as maps (on the JDK, 60 000 channels fit in a 56 MB heap; building the whole list first ran out of memory at 256 MB). - The loopback server never lets an
Errorreach Android's default handler (which kills the app): request bodies are read directly (no NanoHTTPD temp file, whose failure is a barejava.lang.Error), a request thread that cannot be started drops that connection only, and anErroron a request thread ends that request. - Engine requirements measured on the shipped bundle: the JavaScript needs a
Chrome ≥ 94 engine (class
static {}blocks), the CSS looks right from Chrome 111 (color-mix()). The Fire TV Stick (Fire OS 7) ships a recent Amazon WebView and runs the bundle.
# reference backend in Node + mock provider (Xtream user/pass, M3U) serving a built PWA
WWW=/path/to/dist/apps/web tv-shell/devserver/run-dev.sh # http://127.0.0.1:43330
# in a browser: F9 = Back, F10 = Menu, F8 = Play/Pause, F7 = external player
# remote-control tests (Playwright, also run by the workflow) against the dev server
node tv-shell/tests/tv-remote.mjs ; node tv-shell/tests/tv-longlist.mjs
# native document-picker regression: a debug APK must already be installed
# on an English-language Android emulator (physical devices are excluded)
ANDROID_HOME=/path/to/sdk node tv-shell/tests/android-file-input.mjs emulator-5554
# native bridge isolation and main-frame commands
ANDROID_HOME=/path/to/sdk node tv-shell/tests/android-bridge.mjs emulator-5554
# Xtream connection-test refusal, TLS, and redirect evidence
ANDROID_HOME=/path/to/sdk node tv-shell/tests/android-protocol-discovery.mjs emulator-5554
# native background playback: keep the mock provider running for the synthetic clip
ANDROID_HOME=/path/to/sdk node tv-shell/tests/android-background-media.mjs emulator-5554
# the real Java backend on the JDK, without Android (same classes as the APK)
cd tv-shell/android && ./gradlew :backend:check
# regenerate the parser fixtures after an iptv-playlist-parser upgrade
node tv-shell/tools/gen-parser-fixtures.mjstv-shell/
├── android/ Gradle project
│ ├── backend/ pure-Java library: M3U parser, /api routes, Xtream/Stalker helpers (+ JVM tests)
│ └── app/ Android app: NanoHTTPD server, WebView, settings, JS bridge, external player
│ ├── debug.keystore versioned debug signing key (password "android")
│ └── src/main/assets/tvshell/ tv.js (the remote-control layer) and THIRD-PARTY-NOTICES.txt
├── devserver/ reference backend in Node + mock provider, to develop and test on a PC
├── tests/ Playwright tests: driving the real app with remote keys
├── tools/ parser fixture generator (from the real JS parser)
└── scripts/bundle-pwa.sh copies the built PWA into the assets
- Experimental; tested on a Fire TV Stick Lite (Fire OS 7, 1 GB). Generic icon
and banner;
versionNameis bumped by hand. - Only
http/httpsprovider URLs; no EPG, downloads or embedded MPV (PWA limits). - Playlists and portal answers larger than an eighth of the app's memory (about
32 MB on a 1 GB stick) are refused with
ERR_RESPONSE_TOO_LARGE. - The loopback server is reachable by other apps on the device (see the security model).
The diagnostics (long-press Menu or hold Back → IPTVnator settings, or the error panel) show the WebView's Chrome engine version, the device, the embedded server port and a "Screen" line (pixels, density, CSS width, layout width). Attach it to any report. From a PC:
adb connect 192.168.x.x
adb logcat -s TvShell chromium # [console] = page errors; "web app ready" = the app started
adb shell dumpsys package com.amazon.webview.chromium | grep versionName- "App not installed" /
INSTALL_FAILED_UPDATE_INCOMPATIBLE: the installed APK was signed with another key (a local build, another fork). Uninstall it first (Settings → Applications → Manage installed applications, oradb uninstall org.tvshell.iptvnator); builds from this repository share one key and update each other withadb install -r. - Downloader refuses to install: enable Apps from unknown sources (or
Install unknown apps → Downloader) in Developer options and use the direct
.apklink of the release. adb connectrefused: enable ADB debugging, accept the prompt on the TV at the firstadbcommand; the IP is under About → Network.
- "The player could not be loaded" after 30 to 60 s: the page did not start. If the Chrome version shown is below 94, the WebView cannot run the Angular bundle; update "Amazon WebView" / Android System WebView from the Appstore (GeckoView would be the fallback). Between 94 and 110 the app works with degraded colours. The last console error is shown under the message.
- "The system web view is unavailable": Amazon WebView / Android System WebView is being updated or was disabled. Wait a minute and press Retry, or update (re-enable) it.
- "The embedded server could not start": ports 43330 to 43334 are taken by
another application; close it or restart the stick. The chosen port is
remembered because the app's data is bound to
http://127.0.0.1:port. - "Port 43330 is used by another app" toast, empty app: the app runs on a temporary port, so the saved playlists are not visible. Close the other app (or restart the stick) and come back to IPTVnator; nothing was lost. Do not add your sources again meanwhile: they would stay on the temporary port.
- "The player was restarted after it ran out of memory": the WebView's renderer was killed; the app reloads from its start view (the last view only when IPTVnator's start-up setting restores it). If it keeps happening, use a smaller playlist (hide categories in IPTVnator). After Android closed the app in the background, it reopens on the page it was showing.
- The picture does not fit the screen, or is too small: the scale is
CSS width ÷ layout width(on a 1080p Fire TV: 960 ÷ 1280 = 0.75). Check the "Screen" line of the diagnostics and change Interface size (960 = large, 1280 = normal, 1600 = dense). If it is still cut off, report the "Screen" line and the build. - Less animation than in a browser: on purpose.
tv.jsturns off the focus-ring transitions, the LIVE pulse, the empty-state bobbing and the dashboard hero's crossfade and zoom (the slideshow still advances, as a cut): each of them costs a frame of work per screen refresh on a Stick. To compare, runlocalStorage.setItem('tvshell.fullMotion', '1')in the WebView console (chrome://inspect) and reload;removeItemturns them off again. - An HLS channel misbehaves with the HTML5 or ArtPlayer engine only: those
engines demux MPEG-TS in hls.js's worker.
localStorage.setItem( 'iptvnator.hlsInlineTransmux', '1')and a reload keep it on the page's thread, as before, to compare.
- A key does nothing:
adb shell getevent -lshows the code the remote sends; the intercepted keys are listed inMainActivity.dispatchKeyEvent(Back, Menu and its aliases, play/pause, rewind, fast-forward), everything else goes to the page (tv.js). - No Menu key (Google TV, most Android TV boxes): hold Back for the shell settings.
- No visible focus: press an arrow, the remote layer puts the focus back near its last position. Back closes dialogs and menus first, then walks the history, then exits (two presses).
- No keyboard on a field: OK on the field opens the Fire TV keyboard;
otherwise
adb shell input text "...".
- Silent / black stream, "This stream cannot be played here": codec or
container the WebView does not handle (raw
.ts, AC3...). Accept the VLC prompt (install VLC from the Appstore), or Menu on the picture / "Copy stream URL". For Xtream, choose them3u8output format in the playlist settings. Each new attempt on a channel asks again. - The VLC prompt no longer appears: IPTVnator settings → External player → tick "Offer an external player" and pick the player. With the HTML5 or ArtPlayer engine, a dropped connection (native media error 1 or 2) does not offer it: VLC would fail the same way. The default video.js engine still offers it, because it reports a manifest the page may not read (no CORS) as that same network error, and VLC plays those.
- "Error, something went wrong (ERR_RESPONSE_TOO_LARGE)" when adding a source: the playlist or portal answer is larger than the device can hold; use a smaller export (one country, no VOD) or an Xtream portal.
- The screensaver starts on a list: intended; the screen stays on while a stream plays.
- Start over: IPTVnator settings → Erase IPTVnator data (or uninstall the app). Data survives updates as long as the server port does not change.
- Inspect the page: IPTVnator settings → remote debugging, then
chrome://inspecton a PC withadb connect.
The APK ships NanoHTTPD (BSD 3-Clause), AndroidX WebKit (Apache 2.0) and a
JavaScript port of Android's FocusFinder (Apache 2.0). The notices are in
android/app/src/main/assets/tvshell/THIRD-PARTY-NOTICES.txt, also served at
/tvshell/THIRD-PARTY-NOTICES.txt. The web app's own licenses are in its
3rdpartylicenses.txt.