hdhriptv
Turn any M3U/IPTV playlist into a HDHomeRun tuner that Plex, Jellyfin, Emby, and other DVR software discover automatically.
What is hdhriptv?
hdhriptv is a lightweight server that makes your IPTV playlist look like a physical HDHomeRun TV tuner on your network. HDHomeRun is the most widely supported network TV tuner standard -- nearly every DVR application can detect and use one automatically. hdhriptv takes advantage of that by speaking the same protocol, so you can plug your IPTV channels into the DVR workflow you already use without any special client-side setup.
Point hdhriptv at an M3U playlist, curate the channels you want to publish through a built-in admin UI, and your DVR will discover it automatically -- just like a real tuner. From there, your DVR handles the rest -- guide data, recordings, live TV, and multi-device playback -- all using channels from your IPTV subscription.
hdhriptv is written in Go with an embedded SQLite database -- no external database required. It runs as a single binary, in Docker (multi-arch: amd64 and arm64), or as a systemd service. The only external dependency is ffmpeg, needed for the default ffmpeg-copy stream mode. If you use direct proxy mode, no ffmpeg is required. The Docker image includes ffmpeg out of the box.
Key Features
Channel Management
- Import channels from any M3U/IPTV playlist
- Curate which channels to publish through a web-based admin UI
- Attach multiple sources per channel with ordered failover priority
- Dynamic channel rules that automatically sync matching catalog items into channel sources
- Dynamic channel blocks for bulk channel creation from filter queries
- Merge/duplicate detection to identify overlapping catalog entries
Streaming
- Three stream modes: direct proxy, ffmpeg remux (copy), and ffmpeg transcode
- Shared sessions -- multiple viewers on the same channel share one upstream connection
- Automatic stall detection and source failover with configurable recovery policies
- Recovery filler (null packets, PSI, or decodable A/V slate) keeps clients connected during failover
- Configurable tuner count to match upstream provider limits
Discovery and Compatibility
- HDHomeRun UDP discovery (port 65001) -- detected automatically by DVR software
- UPnP/SSDP discovery with full device description and ContentDirectory browsing
- Standard lineup endpoints:
/discover.json, /lineup.json, /lineup.xml, /lineup.m3u
- Optional legacy HTTP listener on port 80 for clients that ignore advertised ports
- Optional mDNS/Avahi integration for .local hostname resolution
DVR Integration
Every HDHomeRun-compatible app discovers hdhriptv automatically. For deeper integration, Channels DVR and Jellyfin get dedicated sync and refresh workflows:
- Forward sync: push hdhriptv channel mappings into your DVR's custom lineup
- Reverse sync: pull DVR-side mappings back into hdhriptv
- Per-channel DVR mapping control with lineup and station-ref support
- Jellyfin-native lineup refresh after playlist updates
- Channels DVR custom mapping API support
Automation
- Scheduled playlist refresh via cron expressions
- Auto-prioritize: probe sources and reorder by quality/availability
- Source health tracking with cooldown ladders for failing sources
- Background source probing at configurable intervals
- All jobs observable through the admin API with progress and status tracking
Admin and Observability
- Web UI for catalog browsing, channel management, tuner status, and automation
- Rapid source-add workflow for quickly building channel lineups
- Live tuner and session dashboard with recovery trigger controls
- Session history console with filterable diagnostics
- Full JSON API for scripting and integration
- Health check endpoint (
/healthz) for load balancers and orchestrators
- Optional Prometheus metrics endpoint (
/metrics)
- Per-client IP rate limiting with trusted proxy support
Compatibility
hdhriptv works with any software that supports HDHomeRun tuners. It is tested with the following DVR and media platforms:
| Platform |
Discovery |
Live TV |
DVR/Recording |
Notes |
| Plex |
Automatic (HDHomeRun scan) |
Yes |
Yes |
Also supports manual IP entry |
| Channels DVR |
Automatic (HDHomeRun scan) |
Yes |
Yes |
Deepest integration: forward/reverse sync, custom lineup mapping |
| Jellyfin |
Automatic (HDHomeRun tuner) |
Yes |
Yes |
Native lineup refresh integration; use port 80 if detection fails |
| Emby |
Automatic (HDHomeRun tuner) |
Yes |
Yes |
Use port 80 if detection fails |
| VLC |
Direct URL |
Yes |
-- |
Open any /auto/v{channel} URL directly |
| UPnP apps |
SSDP/ContentDirectory |
Browse/Play |
-- |
BubbleUPnP, Kodi UPnP browser, and similar (read-only browse) |
Stream mode compatibility: ffmpeg-copy (the default) provides the best balance of compatibility and efficiency. Use ffmpeg-transcode if clients have trouble with your source codec/container format. Use direct for lowest overhead when sources already produce compatible MPEG-TS output.
Quick Start
Prerequisites
- Go
1.25.4 or newer (see go.mod)
- Network reachability from client devices to this service
ffmpeg installed if using:
STREAM_MODE=ffmpeg-copy (default)
STREAM_MODE=ffmpeg-transcode
- No
ffmpeg required if using STREAM_MODE=direct
Run Locally (Linux and Mac)
You can either download a prebuilt release binary for Linux
(hdhriptv-linux-amd64 / hdhriptv-linux-arm64) or macOS
(hdhriptv-darwin-amd64 / hdhriptv-darwin-arm64) from:
https://github.com/arodd/hdhriptv/releases
-
Download the release binary for your platform from the release page above.
-
Make the downloaded binary executable:
chmod +x ./hdhriptv-*
- Or build locally from source instead:
go build -o hdhriptv ./cmd/hdhriptv
- Run with minimum practical settings (replace
./hdhriptv with your downloaded filename if not building locally):
PLAYLIST_URL="https://example.com/playlist.m3u" \
ADMIN_AUTH="admin:change-me" \
./hdhriptv
- Verify service health and discovery:
curl -s http://127.0.0.1:5004/healthz
curl -s http://127.0.0.1:5004/discover.json
curl -s http://127.0.0.1:5004/lineup.json
- Open
http://127.0.0.1:5004/ui/catalog and publish channels from catalog items.
lineup.json is channel-only and remains empty until at least one channel is published.
Run With Docker
By default, use the public Docker Hub image:
docker pull arodd/hdhriptv:latest
Run container:
docker run --rm -it \
-p 5004:5004/tcp \
-p 65001:65001/udp \
-p 1900:1900/udp \
-v hdhriptv-data:/data \
-e PLAYLIST_URL="https://example.com/playlist.m3u" \
-e ADMIN_AUTH="admin:change-me" \
-e UPNP_ENABLED=true \
arodd/hdhriptv:latest
Notes:
- The image includes
ffmpeg.
- The runtime image uses
alpine:3.23 and installs ffmpeg from Alpine's community repository.
- Multi-arch image available (amd64, arm64).
- SQLite state persists in
/data when a volume is mounted.
- Add
-p 80:80/tcp -e HTTP_ADDR_LEGACY=:80 if you need legacy client compatibility.
Optionally, build and run from the local Dockerfile instead:
docker build -t hdhriptv:local .
docker run --rm -it \
-p 5004:5004/tcp \
-p 65001:65001/udp \
-p 1900:1900/udp \
-v hdhriptv-data:/data \
-e PLAYLIST_URL="https://example.com/playlist.m3u" \
-e ADMIN_AUTH="admin:change-me" \
-e UPNP_ENABLED=true \
hdhriptv:local
Build and publish the same multi-arch image flow used in GitLab CI from your local machine (run docker login first):
make release-local REGISTRY_IMAGE=registry.example.com/org/hdhriptv IMAGE_TAG=latest
release-local now uses the same pinned BINFMT_IMAGE validation as the main
release workflow. The default helper image is already pinned; if you override
it, use an explicit version tag or digest instead of a floating ref such as
:latest.
For a private registry shortcut, copy Makefile.local.example to the ignored
Makefile.local, set INTERNAL_REGISTRY_IMAGE, commit the tracked worktree,
then run make release-local-internal. Private registry coordinates stay out
of the public Makefile, and the shortcut defaults to the immutable
git-<12-character commit> image tag instead of latest.
Run on Windows
-
Download the Windows binary release (hdhriptv-windows-amd64.exe).
-
Install ffmpeg and ffprobe (Windows builds):
-
Extract the build and point both flags to executable paths (not just the bin directory):
--ffmpeg-path C:\Users\<you>\ffmpeg\bin\ffmpeg.exe
--ffprobe-path C:\Users\<you>\ffmpeg\bin\ffprobe.exe
-
Common startup pattern:
.\hdhriptv.exe `
--playlist-url https://example.com/playlist `
--ffmpeg-path C:\users\person\ffmpeg\bin\ffmpeg.exe `
--ffprobe-path C:\users\person\ffmpeg\bin\ffprobe.exe `
--http-addr-legacy :80 `
--friendly-name "HDHRIPTV Windows"
Stream Modes and Tradeoffs
| Mode |
ffmpeg Required |
CPU Use |
Compatibility |
Notes |
direct |
No |
Lowest |
Lowest/variable |
Byte-for-byte upstream proxy; codec/container unchanged. |
ffmpeg-copy |
Yes |
Low |
High |
Remux to MPEG-TS without transcoding. Good default. |
ffmpeg-transcode |
Yes |
Highest |
Highest |
Transcodes to H.264/AAC MPEG-TS for difficult clients. |
Configuration Reference
The most common settings are listed below. For the full reference including all flags, internal stream tuning, and SQLite diagnostic toggles, see docs/CONFIGURATION.md.
| Flag |
Environment Variable |
Default |
Notes |
--playlist-url |
PLAYLIST_URL |
empty |
M3U playlist URL. If empty, no automatic refresh runs. |
--playlist-max-response-bytes |
PLAYLIST_MAX_RESPONSE_BYTES |
67108864 |
Per-source remote playlist response cap. |
--db-path |
DB_PATH |
./hdhr-iptv.db |
SQLite file location. |
--http-addr |
HTTP_ADDR |
:5004 |
Primary HTTP listener. |
--http-addr-legacy |
HTTP_ADDR_LEGACY |
empty |
Optional second listener, often :80. |
--tuner-count |
TUNER_COUNT |
2 |
Max concurrent stream sessions. |
--traditional-guide-start |
TRADITIONAL_GUIDE_START |
100 |
First guide number assigned to traditional channels. Must be between 1 and 9999, and startup rejects values whose effective traditional guide range would overlap the reserved dynamic guide range (10000+). Existing traditional channels are renumbered on startup when this value changes. |
--friendly-name |
FRIENDLY_NAME |
HDHR IPTV |
Displayed in discover payloads and DVR UIs. |
--stream-mode |
STREAM_MODE |
ffmpeg-copy |
direct, ffmpeg-copy, ffmpeg-transcode. |
--ffmpeg-path |
FFMPEG_PATH |
ffmpeg |
Executable used for ffmpeg stream modes. |
--ffprobe-path |
FFPROBE_PATH |
ffprobe |
Executable used by auto-prioritize analysis and shared-session stream profile probes. |
--admin-auth |
ADMIN_AUTH |
empty |
user:pass format. Required by default when any HTTP listener is non-loopback. |
--allow-open-admin |
ALLOW_OPEN_ADMIN |
false |
Temporary insecure compatibility escape hatch for non-loopback listeners without ADMIN_AUTH. |
--refresh-schedule |
REFRESH_SCHEDULE |
empty |
Cron expression for playlist sync. |
--initial-playlist-sync-attempt-timeout |
INITIAL_PLAYLIST_SYNC_ATTEMPT_TIMEOUT |
5m |
Per-attempt startup playlist-sync timeout. |
--initial-playlist-sync-retry-budget |
INITIAL_PLAYLIST_SYNC_RETRY_BUDGET |
6m |
Total startup playlist-sync retry budget. |
--upnp-enabled |
UPNP_ENABLED |
true |
Enable UPnP/SSDP discovery on UDP 1900. |
--stall-detect |
STALL_DETECT |
4s |
Soft no-publish threshold that starts subscriber filler without canceling the producer. |
--stall-failover-grace |
STALL_FAILOVER_GRACE |
4s |
Additional producer/FFmpeg reconnect grace before hard recovery; 0 restores legacy behavior. |
--stall-timing-strict |
STALL_TIMING_STRICT |
false |
Reject timeout/reconnect settings whose bounded FFmpeg recovery budget exceeds the combined hard-failover threshold. |
--stall-policy |
STALL_POLICY |
failover_source |
failover_source, restart_same, close_session. |
--recovery-filler-mode |
RECOVERY_FILLER_MODE |
slate_av |
null, psi, or slate_av (decodable A/V filler). |
--recovery-filler-grace-mode |
RECOVERY_FILLER_GRACE_MODE |
null |
Decoder-neutral filler (null/psi) during the stall-failover grace window; none starts the full filler immediately. |
--quality-floor-min-bitrate-bps |
QUALITY_FLOOR_MIN_BITRATE_BPS |
0 |
Minimum sustained source bitrate before quality-floor recovery triggers (0 disables; per-channel/dynamic-block overrides via quality_floor). |
--quality-floor-min-resolution |
QUALITY_FLOOR_MIN_RESOLUTION |
0 |
Minimum source video height in pixels before quality-floor recovery triggers (0 disables). |
--quality-floor-min-fps |
QUALITY_FLOOR_MIN_FPS |
0 |
Minimum source frame rate before quality-floor recovery triggers (0 disables). |
--source-profile-freshness |
SOURCE_PROFILE_FRESHNESS |
24h |
Persisted profile freshness before a new post-graduation ffprobe. |
--enable-metrics |
ENABLE_METRICS |
false |
Enables /metrics Prometheus endpoint. |
--log-level |
LOG_LEVEL |
info |
trace, debug, info, warn, error. |
Upgrade note: initial ffmpeg + slate_av startup now uses the same
audio/video and random-access admission contract as recovery, and source health
success is recorded only after the 20-second probation window. Set
STARTUP_RANDOM_ACCESS_RECOVERY_ONLY=true, CYCLE_FAILURE_MIN_HEALTH=0, or
STALL_FAILOVER_GRACE=0 only as targeted compatibility rollbacks. See
docs/RECOVERY.md for timing and rollout guidance.
Short sessions that remain healthy through subscriber-free idle teardown record
a clean_idle success after the smaller of the probation and idle windows,
floored at half the probation window (10 seconds with defaults). This lets
healthy shorter viewing clear stale source failure state without letting a
two-second surf followed by the five-second idle timer launder early failures.
Documentation
| Document |
Description |
| docs/API.md |
API and endpoint reference |
| docs/ARCHITECTURE.md |
Runtime architecture, route map, and subsystem wiring |
| docs/AUTOMATION.md |
Job runner, scheduler, and background automation behavior |
| docs/CATALOG-PIPELINE.md |
M3U ingest, normalization, and reconcile pipeline details |
| docs/CHANNELS.md |
Channel/favorites service model and dynamic block flows |
| docs/CONFIGURATION.md |
Full flag and environment variable reference |
| docs/DEVELOPMENT.md |
Testing, CI, and GitHub mirror publish workflow |
| docs/DATA-MODEL.md |
SQLite data model, keys, and persistence semantics |
| docs/DVR-INTEGRATION.md |
Channels DVR and Jellyfin sync workflows and edge cases |
| docs/OPERATIONS.md |
Operations, troubleshooting, and advanced topics |
| docs/RECOVERY.md |
Stall detection, failover, and recovery filler behavior |
| docs/SESSION-LIFECYCLE-BASELINE.md |
Shared-session lifecycle baseline evidence workflow |
| docs/STREAMING.md |
Shared-session streaming pipeline internals and tuning |
| docs/UI-COMPONENTS.md |
Admin UI pages, templates, and frontend architecture |
| deploy/systemd/README.md |
Systemd deployment |
| deploy/avahi/README.md |
Avahi/mDNS setup |
| deploy/testing/compatibility-checklist.md |
Client compatibility testing |
Deployment Notes
- Docker: See Quick Start above. Multi-arch image (amd64, arm64). SQLite state persists in
/data.
- Systemd: See deploy/systemd/README.md for unit file and setup guide.
- Avahi/mDNS: See deploy/avahi/README.md for optional
.local hostname resolution.
- Ports: TCP
5004 (main), UDP 65001 (discovery), optionally UDP 1900 (UPnP) and TCP 80 (legacy).
Testing
See docs/DEVELOPMENT.md for test commands, smoke checks, client compatibility testing, and the GitHub mirror publish workflow.