Platform tour · self-hosted IPTV, end to end

SarmaPlayer

A high-performance native Kotlin player for Android TV — multi-playlist, live TV with a full EPG, movies and series — now joined by native Android and iOS mobile apps, a browser player as the no-install alternative, owner and staff dashboards, real Stripe checkout for licenses, an API that treats every device as a cryptographic identity, and a stream relay that reaches providers the datacenter can't. Deployed with one command onto a Cloudflare-edge + single-VPS stack with zero inbound ports.

LIVE TVEPG GUIDEVOD SERIESMULTI-PLAYLISTFAVORITESMOBILEPAID LICENSES
TESTFLIGHT
play.sarmaplayer.com
SarmaPlayer IPTV PLAYER
What do you want to watch?
Live TV906 channels
Movies10,197 titles
Seriesfull seasons
Living Room · Firestick Max·v0.9.0
HomeGuideSearchYou
Android TV / Fire TV · D-pad lean-back Web player · zero install Android mobile · phone + tablet iOS mobile · TestFlight beta
9 versioned apps 18 Android TV releases → v0.10.5 4.8 MB signed TV APK ~750 backend test cases 5 services, one deploy command 4 UI languages 0 open inbound ports real Stripe checkout, live

Platforms

Seven surfaces live, iOS in TestFlight, two on the bench

Android TVnative Kotlin · Compose LIVE NOW
Fire TVDownloader install · self-update LIVE NOW
Web playerany browser · zero install LIVE NOW
Manageowner dashboard + landing LIVE NOW
Operatorstaff console · live census LIVE NOW
Android mobilephone + tablet app LIVE NOW
iOS mobileiPhone + iPad app TESTFLIGHT
Samsung Tizennative TV port IN THE WORKS
LG webOSnative TV port IN THE WORKS

Architecture

Every packet has a lane

Viewers enter through Cloudflare and a tunnel — the VPS never exposes a port. Content leaves through the relay's egress ladder, which picks the cheapest path a provider will actually answer: the VPS itself, a named proxy, or the customer's own TV acting as a courier.

Screens Android / Fire TV Compose TV · Media3 R8 → 4.8 MB · self-update Web player React · hls.js · mpegts.js web.sarmaplayer.com Manage owner dashboard + landing sarmaplayer.com Operator staff console · live census operator.sarmaplayer.com Cloudflare edge Pages ×3 web · manage · operator R2 + CDN APK + update manifest nightly DB backups Tunnel outbound-only cloudflared no inbound ports, ever VPS · Coolify API Hono 4 · Drizzle · Zod 4 Better Auth · jose (ES256) JWKS · fail-closed /ready Postgres 16 drizzle migrations, gated Redis 7 · BullMQ ingest · EPG · billing workers Stream relay undici proxy · ffmpeg remux signed stream tokens (eg) SSRF vetting · Range/VOD Egress ladder → providers 1 · Direct VPS default; some CDNs block DCs 2 · Named proxies e.g. Tailscale exit sidecars 3 · Device couriers TVs fetch geo-locked catalogs Providers Xtream · M3U catalogs live + VOD streams XMLTV / EPG + scrapers 60 s liveness heartbeats → operator census
ingress — viewer & control traffic egress — provider fetch & streaming telemetry — heartbeats & self-update

User journey

From cardboard box to live TV in minutes

The whole activation funnel is built to survive a non-technical customer on a hotel Wi-Fi — every step recovers, refreshes, or explains itself.

  1. 1
    Install

    The landing page walks the TV install: free Downloader app, code 5210303, and the signed 4.8 MB APK arrives from the CDN. In a browser? web.sarmaplayer.com — nothing to install at all.

  2. 2
    First boot

    The TV mints its hardware identity key and bootstraps in one round-trip: a device ID, a running trial entitlement, and a pairing code — before the user touches a button.

  3. 3
    Pair

    Scan the QR or type the short SP- code into the phone dashboard. The TV polls until claimed and auto-mints a fresh code before the old one can expire — nobody ever stares at a dead code.

  4. 4
    Add content

    Paste an Xtream login or M3U link. The server ingests and normalizes the catalog, and a TV guide is matched and attached automatically the moment the playlist is created.

  5. 5
    Watch

    D-pad zapping with now/next, a full EPG guide, Movies and Series with resume — streaming direct from the provider or through the relay, whichever policy says.

  6. 6
    Stay running

    Trial ends → an activation code unlocks a license. New releases surface as a one-press self-update. And if setup ever fails, the diagnostics card names the exact fault and offers the fix — the same screen that let us repair a customer's TV from two photos.

The surfaces

Nine apps, one commit discipline

Every app carries its own generated changelog and version tags, cut from conventional commits by the platform's own CLI.

Android TV player

Kotlin · Jetpack Compose TV · Media3 · minSdk 21 → SDK 35

  • High-performance native player: R8-shrunk to 4.8 MB, baseline profiles, async codec queueing, D-pad-first UI
  • Multi-playlist — keep several providers, switch the active one instantly
  • Hardware-backed device identity with a 5-tier provisioning ladder — survives broken OEM keystores
  • In-app self-update from the R2 manifest; diagnostics card with a self-update escape hatch
  • Live TV + EPG guide, Movies/Series, favorites, EN/DE/HR

Android mobile

Kotlin · Jetpack Compose · Media3 · Room · Play internal track

  • Same catalog engine as TV — phone/tablet layouts, not a separate backend
  • Pull-to-refresh + refresh-on-resume on Home, Live and Playlists; a searchable in-player channel list that follows the category you're watching
  • Server-side adult-content gate; friendly update banner on Play and sideloads

iOS mobile

Swift · SwiftUI · AVPlayer + VLCKit dual engine · TestFlight

  • Pair-to-activate onboarding, animated expired-license gate, one-tap demo catalog
  • Player chrome v3 with external-player hand-off, AirPlay, live channel zapping + EPG overlay, watch progress
  • iPad-adaptive layouts, parental PIN, EN/DE/HR; automated App Store submission via asc

Web player

React · Vite · Tailwind · hls.js + mpegts.js · i18next

  • Passwordless email-OTP / Google sign-in → auto-provisioned web device
  • Netflix-style overlay stack; keyboard, touch and swipe-zapping on phones
  • Session handoff from the dashboard via URL hash — no re-login

API

Hono 4 · Drizzle ORM · Zod 4 · Better Auth · BullMQ · jose

  • Module-first layering (routes → service → repo), coded errors, OpenAPI
  • Dual session families: device (challenge → signed session) and manage — one guard accepts both
  • ~700 test cases; CI gates prettier + eslint + typecheck + tests + dependency-cruiser

Stream relay

Hono · undici ProxyAgent · ffmpeg remux · jose

  • Short-lived signed stream tokens carry the egress policy as an eg claim
  • Named egress proxies for providers that block datacenter IPs
  • SSRF vetting, timeout budgets, HTTP Range for seekable VOD

Manage

React SPA · Cloudflare Pages · sarmaplayer.com apex

  • Playlists, devices, EPG assignment, activation — plus the public landing
  • Guided TV install: Downloader code + hosted APK, one modal
  • EN/DE/HR/PT, phone-first responsive pass

Operator

React SPA · staff-only · Cloudflare Pages

  • Live online/offline device census from 60-second heartbeats
  • Device-type split: Android vs web (WebOS/Tizen slots ready)
  • Stream ledger with applied egress; billing & device controls

sarma CLI

TypeScript · zero runtime deps · local-only signing

  • sarma deploy production — all five services, watched and health-verified
  • sarma release — bump, sign, upload, publish manifest, purge CDN, tag
  • sarma changelog — per-app versions and changelogs from conventional commits

Kotlin SDK

Pure-JVM core · OkHttp · kotlinx.serialization

  • Device bootstrap, transparent session handshake, catalog client
  • Offline ES256 entitlement verification with hardcoded P-256 params
  • JVM-testable by design; Android keystore lives in the app layer

Ingress · content pipeline

From messy provider data to one clean catalog

Ingestion

Xtream and M3U playlists are parsed server-side only — untrusted input never reaches a client. Normalized live/VOD/series catalogs are versioned and published; devices poll their active playlist's manifest.

Transport ladder

When a provider geo-blocks the server, ingestion climbs a ladder ending with device-assisted fetch: the customer's own TV couriers the provider payload home over its residential connection.

Refresh

BullMQ schedulers keep playlists fresh; a one-press refresh re-syncs list + guide together, with per-device auto-refresh cadence settings.

EPG platform

The guide is a pipeline, not a file

Provider EPG is unreliable, regional, and often simply missing — so SarmaPlayer runs its own canonical guide platform and maps playlists onto it.

Sourcing

Multi-source by design: public and BYO XMLTV feeds, curated per-region source sets (a seeded Balkan lineup of ~1,600 canonical channels from 11 feeds), plus a scraper engine that turns listing sites without any feed into XMLTV — same pipeline downstream, adapters per site.

Ingestion

Feeds normalize into one canonical channel space with priority ordering between overlapping sources; ingestion runs as scheduled BullMQ workers with per-source staleness tracking, operable from the CLI (sarma epg).

Mapping

A matcher ladder binds messy playlist channels to canonical ones: exact tvg-id first, then normalized fuzzy name matching — 179 channels auto-mapped on the first real customer playlist. Guides attach to new playlists automatically at creation; owners can rebind per channel.

Syncing

Schedulers keep every source current and every playlist's guide re-synced with it; devices get inline now / next on zap and a full grid guide, refreshed together with the playlist.

Egress · the streaming path

Streams take the cheapest lane that works

Direct mode

By default players stream straight from the provider — the platform stays out of the data path entirely.

Relay mode

Per device or playlist, streams route through the relay: credentials stay server-side, browsers get CORS-clean playback, and ffmpeg remuxes containers the web can't chew.

Egress policy

Operator-set, resolved device → playlist → default, and stamped into the signed stream token. Unknown proxy names log once and fall back to direct — config drift never kills a stream.

Identity & entitlements

Every device is a key, every right is a signature

Device identity

A non-exportable EC P-256 key minted in the TV's secure hardware, with a defensive ladder (StrongBox → TEE named-curve → key-size → encrypted software key) so broken OEM crypto can never brick onboarding.

Sessions

Devices prove possession via challenge → signed session (DPoP-style). Owners get manage sessions; browser accounts (email OTP / Google) auto-provision web devices. Pairing codes are short-lived and one-use.

Entitlements

Trials and licenses are ES256-signed certificates verified offline against the platform JWKS — playback is fail-closed, and /ready refuses deploys that can't serve keys.

Payments

A real Stripe Checkout flow sells a perpetual license — on success the license activates instantly on the device that paid, no polling or manual step.

Field-tested, remotely. A customer's Fire TV shipped a ROM missing two separate crypto APIs. The diagnostics card put the exact fault on screen, the customer sent two photos, and the self-update escape hatch delivered two targeted releases — device unbricked the same day, no remote access, no reinstall.

Operations

Run like a fleet, staffed like a hobby

One-command ship

sarma deploy production builds and publishes API, relay and all three SPAs, watches Coolify to completion, and health-verifies each surface. Android ships via sarma release: signed build, R2 upload, manifest publish, CDN purge, tag — then devices self-update.

Observability

60-second liveness heartbeats feed the operator census; devices report platform, distribution and app version at bootstrap. The onboarding failure card doubles as remote telemetry a customer can read aloud.

Discipline

Develop-first branching with gated promotions, CI on every PR, migrations as an explicit pre-boot deploy step, nightly Postgres dumps to R2, and per-app changelogs generated from the commit history itself.

Tech index

The full toolbox

Languages
KotlinTypeScriptSQLBash
Android
Jetpack Compose TVMedia3 / ExoPlayerAndroid KeystoreR8 + baseline profilesGradleRobolectricJUnit
iOS
SwiftUISwift ConcurrencyAVPlayerVLCKitSwift Package ManagerXCTest
Web
ReactViteTailwind CSShls.jsmpegts.jsi18next
Backend
Node 24Hono 4Drizzle ORMZod 4Better AuthBullMQjosehono-openapiVitestStripe
Data
PostgreSQL 16Redis 7Room (on-device)DataStore
Streaming
ffmpegundiciHLSMPEG-TSXtream APIM3UXMLTV
Crypto
EC P-256ES256 JWS + JWKSDPoP-style sessionsAES-GCMSHA-256 fingerprints
Infra
Cloudflare PagesR2TunnelCDN + cache purgeCoolifyDockerGitHub Actionswrangler
Workflow
pnpm workspacesTurboESLintPrettierdependency-cruiserconventional commitssarma CLI