hls-restream-proxy

pcruz1905MIT更新放缓

轻量 HLS 转流代理,给 Jellyfin/Emby/Plex 拉 IPTV 直播源时自动注入请求头、重写播放列表并刷新过期 token,解决鉴权失效和播放中断。

Lightweight HLS restream proxy for Jellyfin/Emby/Plex — injects headers, rewrites playlists, auto-refreshes tokens

暂无版本258star18fork同步于 2026-11-07 07:34 UTC

怎么上手

由 README 提炼,执行前请确认
git clone https://github.com/pcruz1905/hls-restream-proxy.git
cd hls-restream-proxy
cp channels.conf.example channels.conf
docker compose -f docker-compose.example.yml up -d
硬件要求按 README 说明

README 未说明;仅要求 Python 3.8+(纯标准库)及 bash、curl、带 PCRE 的 grep,可用 Docker 运行

有 Docker 镜像
下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

轻量 HLS 中转代理,为 Jellyfin 等媒体服务器补上上游所需的请求头并重写播放列表

轻量 HLS 转流代理,给 Jellyfin/Emby/Plex 拉 IPTV 直播源时自动注入请求头、重写播放列表并刷新过期 token,解决鉴权失效和播放中断。

核心能力

  • 为上游 HLS 请求注入 User-Agent 与 Referer 头
  • 重写 m3u8 使 .ts 分片也走代理
  • 按频道缓存抓取到的 m3u8 地址,默认 1 小时
  • 自动学习各上游主机所需的 Referer
  • 用稳定 /channel/<slug> 地址隐藏过期 token

容易踩的坑

默认只绑定 127.0.0.1:8089,媒体服务器在 Docker 桥接网络时需改用网关 IP 并设置 HLS_PROXY_URL;Plex/Emby 不直接读 M3U,需经 Threadfin 或 xTeVe 中转;上游无 BANDWIDTH 标签时 Jellyfin 会按约 20 Mbps 估算并触发转码,需在 channels.conf 第 9 字段或 HLS_DEFAULT_BANDWIDTH 声明真实码率

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

0 个版本

暂无 release 数据(可能没有发版,或尚未同步)。

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    hls-restream-proxy

    Lightweight HLS restream toolkit for self-hosted media servers.

    Many free IPTV/HLS sources require specific HTTP headers (User-Agent, Referer) that media servers don't send. This proxy sits between your media server and the upstream, injecting the required headers and rewriting m3u8 playlists so all segment requests also go through the proxy.

    Components

    FilePurpose
    hls-proxy.pyHTTP reverse proxy that adds headers to upstream HLS requests
    refresh-m3u.shScrapes source pages, extracts m3u8 URLs, writes M3U playlist
    detect-headers.shAuto-detects which HTTP headers a stream requires
    channels.confYour channel list (slug, name, logo, group, source URL)

    How it works

    ┌──────────┐     ┌───────────┐     ┌──────────────┐     ┌──────────┐
    │ Jellyfin │────▶│ hls-proxy │────▶│ upstream HLS │────▶│ segments │
    │          │     │ :8089     │     │ server       │     │ (.ts)    │
    └──────────┘     └───────────┘     └──────────────┘     └──────────┘
                      adds headers:
                      • User-Agent
                      • Referer
    
    1. refresh-m3u.sh generates an M3U file with stable /channel/<slug> URLs pointing to the proxy
    2. When Jellyfin requests /channel/sporttv1, the proxy scrapes a fresh m3u8 URL on the fly (cached for 1 hour)
    3. The proxy injects the required headers and rewrites the playlist so .ts segments also go through it
    4. The proxy auto-learns the correct Referer for each upstream host — no manual configuration needed

    No more expired tokens — the M3U URLs never change, the proxy handles token refresh transparently.

    Requirements

    • Python 3.8+ (stdlib only, no pip packages)
    • bash, curl, grep (with PCRE / -P)

    Quick start

    Docker (recommended)

    git clone https://github.com/pcruz1905/hls-restream-proxy.git
    cd hls-restream-proxy
    
    cp channels.conf.example channels.conf
    # Edit channels.conf with your channels
    
    docker compose -f docker-compose.example.yml up -d
    

    Docker one-liner (no clone)

    docker run -d --name hls-proxy \
      -p 8089:8089 \
      -v ./channels.conf:/app/channels.conf:ro \
      ghcr.io/pcruz1905/hls-restream-proxy:latest
    

    Manual (no Docker)

    git clone https://github.com/pcruz1905/hls-restream-proxy.git
    cd hls-restream-proxy
    
    cp channels.conf.example channels.conf
    # Edit channels.conf with your channels
    
    python3 hls-proxy.py &
    
    # Generate the M3U
    export M3U_OUTPUT=/path/to/jellyfin/config/iptv.m3u
    export HLS_PROXY_URL="http://YOUR_HOST_IP:8089"
    bash refresh-m3u.sh
    
    # Add the M3U file as an M3U Tuner in your media server
    

    Channel config format

    channels.conf — one channel per line, pipe-delimited:

    slug|Display Name|chno|logo_url|Group|source_page_url|mode|referer
    
    • mode: iframe (default) — page has an iframe whose embed contains the m3u8. direct — page itself contains the m3u8 URL.
    • referer: optional override for the Referer header when fetching the embed page.

    See channels.conf.example for details.

    Systemd setup

    User-level services are provided in systemd/:

    # Copy units
    cp systemd/*.service systemd/*.timer ~/.config/systemd/user/
    
    # Edit paths in the service files, then:
    systemctl --user daemon-reload
    systemctl --user enable --now hls-proxy.service
    systemctl --user enable --now refresh-m3u.timer
    
    # Enable linger so services run without an active login session
    sudo loginctl enable-linger $USER
    

    The timer refreshes URLs every 4 hours by default (edit OnUnitActiveSec in the timer).

    Docker / container media servers

    If your media server runs in Docker on a bridge network, the proxy URL must use the Docker gateway IP (not 127.0.0.1). Find it with:

    docker inspect <container> | grep Gateway
    # Typically 172.x.0.1
    

    Then set HLS_PROXY_URL=http://172.x.0.1:8089.

    Finding the required headers (User-Agent & Referer)

    Every streaming site is different. Here's how to figure out which headers your upstream needs:

    Automatic detection (recommended)

    Run the detector tool — it follows the iframe chain, tests every header combination on both the m3u8 playlist and .ts segments, and tells you exactly what you need:

    ./detect-headers.sh "https://streaming-site.com/channel.php"
    

    Output:

    === HLS Header Detector ===
    
    [1/4] Extracting iframe from page...
      iframe: https://embed-domain.com/embed/abc123
      embed host: https://embed-domain.com
    
    [1/4] Extracting m3u8 from embed page...
      m3u8: https://cdn.example.com/hls/abc123.m3u8?s=token&e=123
    
    [2/4] Testing header combinations on m3u8 playlist...
      403  no-UA
      200  UA
      200  UA+Ref(https://embed-domain.com/)
    
    [3/4] Testing header combinations on .ts segments...
      403  no-UA
      403  UA
      200  UA+Ref(https://embed-domain.com/)
    
    [4/4] Recommended configuration:
      User-Agent + Referer
    
      export HLS_PROXY_REFERER="https://embed-domain.com/"
    

    You can also pass a direct m3u8 URL:

    ./detect-headers.sh "https://cdn.example.com/hls/stream.m3u8?token=xxx" --direct
    

    Manual detection

    If the auto-detector can't find the m3u8 (some sites use heavy JS), use browser DevTools:

    Step 1: Open DevTools Network tab

    1. Open the streaming site in Chrome/Firefox
    2. Press F12 → Network tab
    3. Filter by m3u8 or media
    4. Play the stream — you'll see .m3u8 and .ts requests appear

    Step 2: Find the m3u8 request

    Click on the .m3u8 request and look at the Request Headers. Note down:

    • User-Agent — usually a standard browser UA
    • Referer — this is the key one, usually the embed/iframe domain (not the main site)
    • Origin — sometimes needed instead of Referer

    Step 3: Test with curl

    # Get a fresh m3u8 URL from the Network tab, then test:
    
    # Without headers (probably 403)
    curl -o /dev/null -w "%{http_code}" "https://example.com/hls/stream.m3u8?token=xxx"
    
    # With User-Agent only
    curl -o /dev/null -w "%{http_code}" -A "Mozilla/5.0" "https://example.com/hls/stream.m3u8?token=xxx"
    
    # With User-Agent + Referer
    curl -o /dev/null -w "%{http_code}" -A "Mozilla/5.0" \
      -e "https://embed-domain.com/" \
      "https://example.com/hls/stream.m3u8?token=xxx"
    

    Try each combination until you get 200. That tells you which headers are required.

    Step 4: Test .ts segments too

    The playlist (.m3u8) and segments (.ts) may need different headers. Grab a .ts URL from the playlist and repeat the curl test:

    # Get a segment URL from the m3u8 content
    curl -s -A "Mozilla/5.0" -e "https://embed-domain.com/" \
      "https://example.com/hls/stream.m3u8?token=xxx" | grep ".ts" | head -1
    
    # Test that segment
    curl -o /dev/null -w "%{http_code}" -A "Mozilla/5.0" \
      -e "https://embed-domain.com/" \
      "https://example.com/hls/segment-12345.ts"
    

    Step 5: Configure the proxy

    Once you know the required headers:

    export HLS_PROXY_UA="Mozilla/5.0 ..."        # usually the default is fine
    export HLS_PROXY_REFERER="https://embed-domain.com/"  # the iframe/embed host
    

    Quick reference: common patterns

    Site patternUsually needs
    Page → iframe → m3u8Referer = iframe embed domain
    Direct m3u8 with tokenUser-Agent only
    Cloudflare-protectedUser-Agent + Referer + sometimes Origin

    Tip: find the iframe chain automatically

    Most sites follow this pattern: page → iframe → embed page → m3u8

    # Extract the iframe src
    curl -sL -A "Mozilla/5.0" "https://streaming-site.com/channel.php" \
      | grep -oP 'iframe\s+src="\K[^"]+'
    
    # That gives you the embed domain for the Referer
    

    Environment variables

    VariableDefaultDescription
    HLS_PROXY_PORT8089Proxy listen port
    HLS_PROXY_BIND127.0.0.1Bind address (localhost only by default)
    HLS_PROXY_UAChrome UA stringUser-Agent sent to upstream
    HLS_PROXY_REFERER(empty)Fallback Referer header (auto-learned from /channel/)
    HLS_ALLOWED_IPS(empty)Comma-separated client IP allowlist (empty = all)
    CHANNELS_CONF./channels.confPath to channel config file
    HLS_CACHE_TTL3600Seconds to cache scraped m3u8 URLs (per channel)
    HLS_DEFAULT_BANDWIDTH(empty)Fallback BANDWIDTH (bits/sec) advertised in the master playlist when a channel has no value set in channels.conf. Prevents Jellyfin's ~20 Mbps default guess.
    M3U_OUTPUT/tmp/iptv.m3uOutput M3U file path
    HLS_PROXY_URLhttp://127.0.0.1:8089Proxy URL written into M3U

    Media server compatibility

    Media serverM3U supportHow to use
    JellyfinNativeAdd M3U file as a tuner in Live TV settings
    Channels DVRNativeAdd as custom M3U source
    PlexVia proxyUse Threadfin or xTeVe to expose M3U as a virtual tuner
    EmbyVia proxySame as Plex — use Threadfin or xTeVe
    VLCDirectvlc http://YOUR_HOST:8089/channel/sporttv1
    mpvDirectmpv http://YOUR_HOST:8089/channel/sporttv1
    Any HLS playerDirectPoint at http://YOUR_HOST:8089/channel/<slug>

    FAQ

    Why not a Jellyfin plugin? Standalone scripts work with any media server and any player. No .NET dependency, no breakage when Jellyfin updates, and it also works with VLC, mpv, or any HLS-capable player.

    Does this add latency? No. The proxy is passthrough only — it forwards the exact same bytes from the upstream, no transcoding. The only added latency is the network hop through the proxy (typically <1ms on localhost).

    Do I still need to transcode in Jellyfin? The proxy itself never transcodes — it only fixes headers so Jellyfin can fetch the stream. Whether Jellyfin transcodes afterwards depends on the stream and the client:

    • Most free HLS streams are H.264 + AAC, which direct-play on virtually every client. No transcode.
    • If Jellyfin still transcodes, check the "Playback info" overlay in the web client. Common causes:
      1. Bitrate cap too low — Jellyfin defaults to ~20 Mbps when the playlist has no BANDWIDTH hint, so any bandwidth limit below that triggers a transcode. Fix it by declaring the real bitrate — see the bitrate FAQ below.
      2. Unsupported codec (e.g. HEVC on older Chromecasts) — no way around this, the client genuinely can't play it.
      3. Forced transcoding in user settings — Jellyfin Dashboard → Playback → raise the streaming bitrate limits.

    Jellyfin thinks my stream is 20 Mbps when it's really ~6 Mbps (or similar)? This happens when the upstream returns a single-variant media playlist with no #EXT-X-STREAM-INF:BANDWIDTH tag. Jellyfin can't see the real bitrate and defaults to a worst-case ~20 Mbps, which forces transcoding on any client with a stricter bandwidth cap. Same issue that hits ErsatzTV and Dispatcharr outputs.

    Fix: declare the real bitrate as the 9th pipe-delimited field in channels.conf:

    sporttv1|Sport TV 1|1|logo.png|Sports|https://...|iframe||6000000
    

    The proxy then wraps the stream in a thin master playlist with that BANDWIDTH, and Jellyfin reads the correct value. The underlying media playlist stays reachable at /channel/<slug>/media.m3u8.

    Or set a global default for all channels:

    export HLS_DEFAULT_BANDWIDTH=6000000
    

    Does this work with Plex or Emby? Not directly — Plex and Emby don't read M3U files. You need Threadfin or xTeVe between this proxy and Plex/Emby. These tools make M3U sources appear as a local TV tuner (HDHomeRun) that Plex/Emby can use.

    Can I use this with VLC or mpv? Yes. The /channel/<slug> endpoint returns a standard HLS playlist. Any player that supports HLS can play it directly:

    vlc http://localhost:8089/channel/sporttv1
    mpv http://localhost:8089/channel/sporttv1
    

    See also

    • Threadfin — M3U proxy for Plex/Jellyfin/Emby, makes M3U look like a local tuner
    • xTeVe — M3U proxy for Plex DVR and Emby Live TV
    • Dispatcharr — IPTV stream management and distribution
    • Restreamer — Full-featured self-hosted streaming server with web UI

    License

    MIT