Self-hosted HLS stream resolver and m3u8 proxy. Paste a live sports match page URL, resolve it to a tokenized CDN playlist through a local REST API, and copy a direct proxied stream link for VLC, Stremio, mpv, or any HLS client — zero npm runtime dependencies.
- Quick start
- What problem does this solve?
- Features
- Use cases
- How does stream resolution work?
- How does HLS playback work?
- REST API reference
- Stack
- Source layout
- FAQ
- Disclaimer
Requires Node.js 18+ (native fetch and ES modules).
git clone https://github.com/sharoon7171/fctv33-stream-resolver.git
cd fctv33-stream-resolver
npm startOpen http://localhost:8787, paste a match stream page URL, and click Resolve. The built-in player loads the proxied HLS stream automatically.
Resolve via API:
curl "http://localhost:8787/api/resolve-link?url=https://example.com/basketball/lakers-vs-celtics-2187976/live.html"{
"name": "Lakers vs Celtics",
"streamUrl": "https://cdn.example.com/token-…/index.m3u8",
"referer": "https://player.example.com/",
"playableUrl": "http://localhost:8787/api/hls?url=…&referer=…"
}Use playableUrl for any HLS client that cannot set CDN headers, or open streamUrl in VLC/mpv with the returned referer. Set PORT to override the default 8787.
A live sports match page URL is not the stream. The page is a shell — layout, metadata, and links to a hidden data API. The actual m3u8 playlist URL is obfuscated, session-bound, and only accessible with the correct Referer and Origin headers from the iframe player domain.
This project reverse-engineers that chain end to end:
- Scrapes runtime config from match page HTML (API host, site digit, player referer).
- Calls the upstream data API with signed protobuf requests.
- Decodes ROT47 obfuscation and builds an AES-256-CBC tokenized m3u8 URL.
- Proxies all HLS traffic through
/api/hlsso playback works without manual header injection.
Nothing is hardcoded per site — API hosts, player referers, and request signatures are discovered at runtime from page HTML and upstream responses.
| Capability | Detail |
|---|---|
| Match page parsing | Extracts matchId, sportType, site digit, and data API host from any supported FCTV33 match URL |
| Signed API chain | MD5-prefixed path signing, protobuf envelope parsing, geo-aware stream detail |
| Token URL builder | ROT47 decode → AES-256-CBC session token → tokenized CDN m3u8 path |
| HLS proxy | Rewrites m3u8 manifests, strips CDN #EXT-X-START, decodes _ctump / _ctuph segment URLs, unwraps PNG-wrapped MPEG-TS |
| Referer injection | Applies iframe player Referer and Origin on every upstream CDN fetch |
| Browser UI | Built-in web player with hls.js, resolve timing, and exports for Direct, Proxied, VLC, and mpv |
| Marketing → play site | Redirects marketing hosts (e.g. fctv33.com) to the real play domain from g_player_domains before resolving |
| Zero runtime deps | Node.js built-ins only — node:http, node:crypto, native fetch |
Supported sports include football, basketball, tennis, baseball, cricket, hockey, rugby, motorsport, and more. See src/config/site.js for the full sport slug map.
| Scenario | Description |
|---|---|
| Web UI playback | Submit a match page URL at /; the resolver returns stream metadata and starts live playback through the built-in player. |
| External playback | Copy Proxied (playableUrl) into any HLS client, or copy the VLC / mpv CLI lines (direct streamUrl + iframe referer) — see The proxied link. |
| API integration | Call GET /api/resolve-link?url= from scripts, services, or automation. Feed playableUrl to consumers that cannot set CDN headers, or use streamUrl + referer when the client can. |
| Custom front-end | Replace the default UI while keeping the two-endpoint contract. See public/app.js for hls.js wiring against playableUrl. |
| Deep-link resolve | Pass ?url= on the web UI root to resolve a match page on load without manual input. |
| Upstream protocol study | Trace the full resolve path in source: HTML scrape, signed API bootstrap, protobuf parsing, AES token URL construction, and HLS manifest rewrite. |
Deep-link example:
GET http://localhost:8787/?url=https://example.com/football/match-1234567/live.html
GET /api/resolve-link?url= is handled by src/resolve/stream.js. The handler validates the URL, walks the upstream chain, and returns a stream name, raw m3u8 URL, and proxied playback URL.
sequenceDiagram
participant C as API consumer
participant R as /api/resolve-link
participant P as match-page-url
participant A as UpstreamApiClient
participant Page as Match page HTML
participant API as Data API
participant T as stream-token
C->>R: GET ?url=stream page
R->>P: validateStreamPageUrl
P->>Page: fetch HTML (page Referer / Origin)
P->>P: digit, apis-data host from HTML
P->>API: /api/common/params → g_player_domains + iframe player domain
P->>Page: if marketing host, re-fetch play-site HTML
R->>A: geo, match detail, stream detail
A->>API: signed + protobuf requests
API-->>A: obfuscated url, rb-session
A->>T: buildSignedStreamUrl
R-->>C: name, streamUrl, referer, playableUrl
Input must be an individual match page:
https://{any-host}/{optional-locale}/{sport}/{slug}-{matchId}.html
src/upstream/match-page-url.js maps the sport slug to sportType and reads matchId from the slug suffix. An optional locale prefix (en, zh, th, etc.) is skipped. Query param mdata can supply base64-encoded matchId_sportType instead.
The page is fetched with Referer and Origin set to the stream page origin. From the HTML, two values are extracted: the data API host (apis-data\d+\.[a-z0-9.-]+) and the stream site digit (layout:"livestream-{digit}").
Marketing or listing hosts (for example fctv33.com) often use a digit without iframePlayerDomains. In that case parseMatchPageUrl reads g_player_domains[digit] from /api/common/params, builds the play-site URL (path -match-{id} → -{id}, strip trailing -{mm}-{yyyy} before .html, set icg / ilang), and re-fetches that page so the digit and iframe referer come from the real player host.
Two referer values are used downstream:
| Context | Source | Used for |
|---|---|---|
| Page | Stream / play page URL origin | Match page fetch, geo, signature bootstrap, match detail |
| Player | iframePlayerDomains[digit] from common:web:client |
Stream detail, CDN access via /api/hls |
Player domain comes from GET {apiBase}/api/common/params (ROT47-encoded JSON), after any marketing → play-site hop. If no iframe domain exists for the digit on the play page, resolution fails.
src/upstream/api-client.js calls the data API in order:
| Step | Endpoint | Output |
|---|---|---|
| Geo | /api/user/info |
country, continent |
| Signatures | /api/common/bs |
Body-signing keys (cached per match) |
| Match | /sfver{md5prefix}{suffix}/api/match/detail |
Streams with streamId, siteType, name |
| Stream | /api/stream/detail |
Obfuscated url; rb-session response header |
Match detail is signed: params sorted per REQUEST_PARAM_ORDER, six-character MD5 path prefix (src/crypto/request-hash.js), suffix from bootstrap key 0x66. Protobuf bodies are parsed in src/upstream/protobuf.js. The handler picks the first stream with a streamId.
src/crypto/stream-token.js builds the CDN m3u8:
- ROT47-decode the obfuscated URL (drop first eight characters).
- AES-256-CBC encrypt the
rb-sessiontoken. - Insert
token-{encryptedBase64}ainto the path.
The response contains streamUrl (raw upstream m3u8), referer (iframe player origin), and playableUrl (/api/hls with that referer).
CDN m3u8 and segment requests require the iframe player as Referer and Origin. A client cannot fetch them directly from the raw streamUrl. playableUrl routes all HLS traffic through src/hls/proxy.js, which applies the player referer on every upstream fetch and returns a plain HTTP HLS endpoint.
playableUrl (shown as Proxied in the web UI) is a ready-to-play stream link — not a page URL and not an embed. Copy it after resolve and paste it into any app that accepts an m3u8 URL. No extra setup per player; the proxy handles referer headers and manifest rewrite upstream.
VLC and mpv exports use the raw streamUrl with the real iframe referer instead of the proxy:
vlc --http-referrer 'https://player.example.com/' 'https://cdn…/index.m3u8'
mpv --referrer='https://player.example.com/' 'https://cdn…/index.m3u8'Works with VLC, Stremio, mpv, Kodi, PotPlayer, IINA, ffplay, IPTV apps (TiviMate, IPTV Smarters), OBS, Safari, mobile players, Smart TV apps, and any other HLS client that opens network stream URLs.
For playableUrl, the resolver must be reachable from the device running the player: use localhost on the same host, or substitute the host's LAN IP when playing from another machine on the network. /api/hls responses include CORS headers for cross-origin browser access.
flowchart TB
subgraph consumer ["HLS client"]
HLS["Any m3u8 consumer"]
end
subgraph proxy ["src/hls/proxy.js"]
F["fetch upstream"]
D["decode _ctump / _ctuph"]
W["rewrite m3u8 lines → /api/hls"]
U["unwrap PNG → MPEG-TS"]
F --> D
D --> W
D --> U
end
CDN["HLS CDN"]
HLS -->|playableUrl| proxy
proxy -->|Referer + Origin = playerReferer| CDN
W --> HLS
U --> HLS
Each GET /api/hls?url=&referer= request:
- Fetches upstream with CDN headers derived from
referer. - Decodes
_ctump/_ctuphsegment URLs when present (src/crypto/segment-url.js). - Rewrites
#EXTM3Umedia lines to/api/hlsso the consumer never leaves the resolver origin, and strips#EXT-X-START. The CDN’s positiveTIME-OFFSET=4would otherwise force hls.js to start ~41s back on a 45s window. - Unwraps PNG-contained MPEG-TS and returns
video/mp2twhen the sync byte is0x47.
Upstream HTML or non-2xx responses return 502.
The reference client (public/app.js) consumes playableUrl via hls.js or native HLS and does not set CDN headers. Live edge delay is controlled in the player: liveSyncDurationCount: 3 (three 3s segments ≈ 9s behind live), startPosition: -1, and playback starts only after that much buffer is loaded at liveSyncPosition.
| Route | Params | Response |
|---|---|---|
GET /api/resolve-link |
url — match stream page URL |
{ name, streamUrl, referer, playableUrl } or { error } |
GET /api/hls |
url — upstream m3u8 or segment URL; referer — iframe player referer |
Rewritten m3u8 text or MPEG-TS bytes |
Resolves a match stream page URL to a playable HLS link.
| Param | Required | Description |
|---|---|---|
url |
yes | Full match page URL from the browser address bar |
Success (200):
{
"name": "Team A vs Team B",
"streamUrl": "https://cdn…/token-…/index.m3u8",
"referer": "https://player…/",
"playableUrl": "http://localhost:8787/api/hls?url=…&referer=…"
}Errors: 400 for invalid or missing URL; 502 when upstream resolution fails.
HLS proxy endpoint for m3u8 manifests and MPEG-TS segments.
| Param | Required | Description |
|---|---|---|
url |
yes | Upstream m3u8 or segment URL |
referer |
yes | Stream iframe referer for CDN access |
Returns rewritten m3u8 (application/vnd.apple.mpegurl) or MPEG-TS (video/mp2t). Includes CORS headers for browser playback.
| Layer | Component | Role |
|---|---|---|
| Runtime | Node.js (ES modules) | Resolve pipeline and HTTP server |
| HTTP | node:http |
TCP listener; requests adapted to the Web Request API |
| Networking | native fetch |
Match page, data API, and CDN fetches |
| Cryptography | node:crypto |
MD5 request-hash prefix; AES-256-CBC stream token |
| Serialization | Hand-rolled protobuf | API envelope and field parsing |
| Playback | hls.js (CDN) | MSE HLS demux in the reference client |
src/
main.js HTTP server entry
http/router.js Route dispatch
resolve/stream.js /api/resolve-link handler
hls/proxy.js /api/hls proxy and manifest rewrite
upstream/api-client.js Signed data API client
upstream/match-page-url.js URL validation and page scrape
upstream/protobuf.js Protobuf envelope parsing
crypto/rot47.js ROT47 decode
crypto/request-hash.js MD5 path prefix signing
crypto/stream-token.js AES token URL builder
crypto/segment-url.js _ctump / _ctuph segment decode
config/site.js Sport slugs, locale codes, API constants
public/
index.html Web UI
style.css UI styles
app.js hls.js reference player
What is the difference between streamUrl, referer, and playableUrl?
streamUrl is the raw upstream tokenized m3u8 on the CDN — it needs the iframe referer. referer is that player origin. playableUrl is the proxied stream URL with headers handled by /api/hls. Use playableUrl for clients that cannot set referer headers; use streamUrl + referer (or the UI’s VLC/mpv CLI lines) when the client can.
Does this work when the FCTV33 domain changes?
Yes. API hosts and player referers are scraped from match page HTML at runtime — no hardcoded domain list is required. Sport slugs and locale codes are configured in src/config/site.js.
Why zero npm dependencies?
The entire resolve and proxy pipeline uses Node.js built-ins. hls.js is loaded from CDN in the browser UI only — it is not a server runtime dependency.
What sports are supported?
Any sport with a slug in SPORT_SLUGS: football, basketball, tennis, baseball, cricket, motorsport, rugby, american-football, aussie-rules, hockey, badminton, volleyball, fighting, cycling, handball, and others.
This project documents and demonstrates third-party streaming mechanics for educational and research purposes only. It is not affiliated with any broadcaster or rights holder. You are responsible for complying with applicable law and terms of service where you use it. Do not use this tool to circumvent access controls or distribute copyrighted content without authorization.