Self-hosted Node.js HLS stream resolver and m3u8 proxy for ppv.to live pages. Paste a live stream URL, resolve it to a direct M3U8 playlist through a local REST API, run WebAssembly embed decryption, rewrite manifests for browser playback, and watch through a built-in HTTP server web UI.
- Parses ppv.to live page URLs into stream slugs (
/live/…,24/7channels normalized to247-…) - Fetches stream metadata from the ppv.to API and selects the default iframe embed source
- Posts to the pooembed host, loads the player WASM module in a happy-dom sandbox, and extracts the secure
index.m3u8URL - Returns a proxied playback link that loops HLS manifests and MPEG-TS segments through
/api/hls - Rewrites playlist URIs, strips PNG-wrapped transport stream payloads, and blocks poisoned upstream playlists
- Serves a minimal browser UI with hls.js live playback and proxied URL copy
npm install
npm startOpen http://127.0.0.1:8788, copy a live page URL from ppv.to, paste it, and click Resolve.
Requires Node.js 18+ (native fetch). Runs as a local HTTP server on port 8788 by default (PORT and HOST env overrides).
flowchart LR
Browser["Browser UI\npublic/"]
Server["HTTP server\nsrc/server.js"]
Router["API router\nsrc/http/"]
Resolve["Stream resolver\nsrc/stream/"]
Embed["Embed decrypt\nsrc/embed/"]
Hls["HLS proxy\nsrc/hls/"]
PpvApi["ppv.to API"]
Pooembed["pooembed host"]
Cdn["HLS CDN"]
Browser -->|"/api/stream"| Server
Browser -->|"/api/hls"| Server
Server --> Router
Router --> Resolve
Router --> Hls
Resolve --> Embed
Resolve --> PpvApi
Embed --> Pooembed
Hls --> Cdn
- Parse URL —
src/stream/resolve.jsaccepts a full ppv.to URL, path, or slug and normalizes it to a stream URI - Stream metadata —
GET api.ppv.to/api/streams/{uri}returns available embed sources - Embed fetch —
src/embed/decrypt.jsposts the embed path to pooembed and reads theislandresponse header - WASM decrypt — the bundled gasm WASM module runs inside happy-dom with a mocked JW Player surface and yields the secure m3u8 URL
- Proxied link —
{origin}/api/hls?url=…&embed=…is returned for browser-safe live streaming playback
- Upstream m3u8 or segment is fetched with embed referer headers through impit
- Master and media playlists are rewritten so segment URLs route back through
/api/hls - Live media playlists hold back the newest segment to reduce player stall on edge manifests
- PNG-wrapped MPEG-TS payloads are stripped before segments are returned to the client
src/
server.js HTTP entry, binds host and port
config.js ppv.to API base, embed origin, user-agent
http/router.js static UI, POST /api/stream, GET /api/hls
stream/resolve.js URL parse, metadata fetch, resolve orchestration
hls/proxy.js playlist rewrite, segment proxy, TS strip
embed/
decrypt.js pooembed fetch, WASM decrypt, m3u8 extraction
upstream.js upstream fetch client with embed headers
media.js playlist sniffing, poison detection, proxy rules
wasm/ bundled player WASM assets
public/
index.html resolve UI, hls.js player, proxied URL copy
Resolves a ppv.to live page URL to a direct HLS playlist and a local proxied playback link.
Body
{ "url": "https://ppv.to/live/your-stream" }Also accepts contentPath, path, or uri instead of url.
Success response
{
"ok": true,
"uri": "your-stream",
"contentPath": "/live/your-stream",
"embedPath": "your-stream",
"streamUrl": "https://cdn.example/secure/…/index.m3u8",
"proxiedUrl": "http://127.0.0.1:8788/api/hls?url=…&embed=…"
}Error response
{
"ok": false,
"stage": "decrypt",
"error": "decrypt did not produce m3u8 url",
"uri": "your-stream",
"contentPath": "/live/your-stream",
"embedPath": "your-stream"
}Stages: input, meta, source, decrypt.
HLS proxy endpoint for m3u8 manifests and MPEG-TS segments.
| Param | Required | Description |
|---|---|---|
url |
yes | Upstream m3u8 or segment URL |
embed |
no | Embed path used for upstream referer headers |
Returns a rewritten m3u8 manifest or MPEG-TS segment bytes with CORS headers.
Live ppv.to page URLs and equivalent path slugs are supported.
https://ppv.to/live/{slug}
https://ppv.to/live/247-{channel}
Path-only input also works:
/live/{slug}
{slug}
The resolver strips a leading live/ prefix and normalizes 24/7- slugs to 247-.
- Node.js ES modules,
node:http, nativefetch - happy-dom for WASM player sandboxing
- impit for upstream HLS fetches with browser-like headers
- hls.js 1.5.20 (CDN) for in-browser live streaming playback