Node.js resolver for ppv.to live streams. Fetches stream metadata from the public API, replays the pooembed /fetch protobuf handshake, runs the embed WASM decryptor, and proxies HLS playback for the browser UI.
Example live page: ppv.to/live/…
- Introduction
- What gets resolved
- How decryption works
- How the stream is resolved
- Why it cannot play directly in the browser
- How the relay works
- Stack
- Code map
- REST API
- Disclaimer
A ppv.to live URL like https://ppv.to/live/… is not the stream. It is a page shell — event metadata, layout, and a link to a pooembed player host. The HLS playlist URL never appears in the HTML. The player fetches it from the embed origin after a WASM handshake on POST /fetch.
Three origins are involved:
| Layer | Role |
|---|---|
ppv.to (api.ppv.to) |
Stream metadata, default embed source URL |
Embed host (pooembed…) |
/fetch handshake, WASM decrypt, session island |
| CDN (inside M3U8 responses) | Serves .m3u8 playlists and .ts segments |
flowchart LR
A[ppv.to live URL] --> B["GET /api/streams/{uri}"]
B --> C[Default embed source]
C --> D[POST /fetch protobuf]
D --> E[island + WASM decrypt]
E --> F[index.m3u8 on CDN]
F --> G[VLC / MPV / Relay]
This repository reproduces that chain server-side and exposes it via POST /api/stream, GET /api/hls, and a browser UI.
ppv.to hides the CDN playlist behind the embed layer. This project does not decrypt M3U8 text or video segments — it recovers the upstream playlist URL the embed player would use.
| Stage | Input | Output |
|---|---|---|
| Metadata | ppv.to path or full URL | Embed { origin, path } from API default source |
| Handshake | POST {origin}/fetch |
island header + protobuf response body |
| WASM | island + body in gasm.wasm |
https://…/secure/…/index.m3u8 scraped from WASM memory |
| Relay | Direct M3U8 URL | Rewritten playlist/segments through /api/hls |
Not handled here: DRM keys, segment encryption beyond what the upstream CDN already serves, or any ppv.to account logic.
src/embed/context.js parses the default source from the streams API:
https://{embed-host}/embed/{path}
→ { origin: "https://{embed-host}", path: "{path}" }
src/embed/decrypt.js posts a length-prefixed protobuf body encoding embed.path:
POST {origin}/fetch
Content-Type: application/octet-stream
Origin: {origin}
Referer: {origin}/embed/{path}
The response must include an island header. The body is a protobuf blob; field 2 (string wire type) yields a slug used to pick the correct M3U8 from WASM memory.
src/embed/wasm/gasm.js + gasm.wasm run inside a happy-dom Window with stubbed jwplayer, fetch (returns the /fetch body), and embed-relative Request resolution.
set_stream_jw(island, body) is called; the resulting playlist URL is found by scanning linear memory for:
https://{host}/secure/{…}index.m3u8
When a slug is present, the match containing /{slug}/ is preferred.
src/resolve/stream.js orchestrates:
parseInput(url) → fetchMeta(uri) → embedFromSource(source) → resolveEmbedStreamUrl(embed) → relayUrl(origin, streamUrl, embed)
Input — accepts a full https://ppv.to/… URL or a path. Normalizes live/ prefix and 24/7- → 247- slugs.
Metadata — GET {API_BASE}/streams/{uri} (API_BASE is https://api.ppv.to/api in src/env.js). Reads the default entry from data.sources.
Decrypt — resolveEmbedStreamUrl in src/embed/decrypt.js runs the /fetch + WASM chain above.
Proxied URL — relayUrl builds:
{request-origin}/api/hls?url={encoded-m3u8}&embed={path}&embedOrigin={origin}
Request origin comes from req.headers.host — no hardcoded host or port in source.
The resolved M3U8 URL works in VLC and MPV without this server. In-browser playback does not.
HLS support — only Safari plays HLS natively. Chrome and Firefox need hls.js (public/js/app.js).
Cross-origin — M3U8 and .ts segments live on the embed CDN, not on the UI origin. Browsers block or restrict those fetches (CORS, missing upstream headers).
The UI plays through the proxied URL. External players can use the direct URL from the export panel.
src/relay/hls.js serves GET /api/hls.
- Fetch upstream via
impit(src/embed/upstream.js) withReferer: {origin}/embed/{path}andOrigin: {origin}. - Validate body — reject HTML error pages, empty payloads, and poison playlists (
src/embed/media.js). - M3U8 — rewrite media lines and
URI="…"tags insrc/relay/rewrite.js:- Map segment and child-playlist URIs to
{origin}/api/hls?url=…&embed=…&embedOrigin=…. - Drop incomplete trailing live segments (
syncLiveMediaPlaylist) to reduce stall on the live edge.
- Map segment and child-playlist URIs to
- Segments — strip non-TS prefix bytes (e.g. PNG wrapper) in
src/relay/segment.js, then returnvideo/mp2t.
The browser only talks to your host. VLC/MPV can skip the relay and open the direct M3U8.
| Role | Technology |
|---|---|
| Runtime | Node.js, ES modules, native fetch |
| HTTP | node:http |
| Embed WASM sandbox | happy-dom |
| Upstream TLS/client | impit (Chrome fingerprint) |
| Browser HLS (UI) | hls.js 1.5.20 from jsDelivr |
| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
Listen port |
HOST |
all interfaces | Bind address when set |
npm install
npm startStartup logs the listen URL from srv.address() (e.g. http://localhost:3000/).
src/
server.js boot HTTP server
env.js port, API_BASE, USER_AGENT
http/
route.js onReq — /api/hls, /api/stream, static
respond.js json, text, readBody
static.js public asset routes
resolve/
stream.js resolveStream — metadata → WASM → URLs
relay/
hls.js relayHls — fetch, playlist vs segment
rewrite.js rewritePlaylist, syncLiveMediaPlaylist
segment.js segmentBody — TS payload strip
embed/
context.js embedFromSource, embedFromQuery, relayUrl
decrypt.js resolveEmbedStreamUrl — /fetch + WASM
media.js isM3u8Resource, isPoisonPlaylist, okBody, …
upstream.js upstreamFetch
wasm/ gasm.js, gasm.wasm
public/
index.html UI shell
css/app.css
js/app.js resolve, export, hls.js playback
| Body field | Required | Description |
|---|---|---|
url |
yes | ppv.to live URL or path (e.g. https://ppv.to/live/… or /live/…) |
200 success
{
"ok": true,
"uri": "event-slug",
"contentPath": "/live/event-slug",
"streamUrl": "https://cdn.example/secure/…/index.m3u8",
"proxiedUrl": "http://localhost:3000/api/hls?url=…&embed=…&embedOrigin=…"
}200 failure (structured error, not HTTP 4xx)
{
"ok": false,
"stage": "meta",
"error": "upstream 404",
"uri": "…",
"contentPath": "…"
}Stages: input, meta, source, decrypt. CORS: *.
| Query | Required | Description |
|---|---|---|
url |
yes | Absolute upstream URL (M3U8 or segment) |
embed |
yes | Embed path from resolve |
embedOrigin |
yes | Embed origin from resolve |
Returns rewritten M3U8 (application/vnd.apple.mpegurl) or raw TS (video/mp2t). Missing url → 400. Upstream failure → 502 plain text.
| Path | File |
|---|---|
/ |
public/index.html |
/css/app.css |
styles |
/js/app.js |
UI |
This project does not host, store, or distribute media. ppv.to, embed hosts, and CDNs are independent services. The resolver reads public API metadata and calls embed endpoints the same way a browser player would.
You are responsible for complying with copyright law, site terms of service, and local regulations. No warranty. Use only on content you have the right to access.