Reference implementation of a ppv.to HLS stream resolver: reverse-engineered embed handshake, WebAssembly decrypt, protobuf decode, and an HLS proxy layer for integration into a custom player or backend.
ppv.to live pages do not publish a plain m3u8 URL. Playback is gated behind a third-party embed that:
- Returns encrypted stream metadata from a
/fetchendpoint - Decrypts it inside a WebAssembly player module to produce a signed CDN
index.m3u8 - Serves HLS segments that require embed referer headers and are often PNG-wrapped MPEG-TS
A custom player cannot use the page URL directly. It needs a resolver that reproduces the embed protocol and a proxy that normalizes playlists and segments for playback.
| Capability | Implementation |
|---|---|
| Stream metadata discovery | ppv.to REST API → default iframe embed source |
| Embed origin derivation | Parsed from API source.data URL — no hardcoded host |
/fetch protocol replay |
Protobuf-encoded POST body, island session header |
| WASM decrypt | happy-dom sandbox + bundled gasm module → index.m3u8 URL from linear memory |
| Slug-aware extraction | Protobuf field 2 selects the correct CDN path from WASM output |
| HLS upstream access | impit Chrome impersonation with embed Origin / Referer |
| Manifest rewriting | Master and media m3u8 URIs rewritten for proxy chaining |
| Live playlist handling | Incomplete tail segment trimmed by EXTINF vs TARGETDURATION ratio |
| Segment decode | PNG container stripped to raw 0x47 MPEG-TS sync bytes |
| Poison playlist detection | Static-image and empty-media manifests rejected before playback |
Import the resolver and proxy modules into your own backend and player. The bundled HTTP server and web page are for local pipeline testing.
flowchart TB
Player["Your player / app"]
Resolve["Resolve API"]
PpvApi["api.ppv.to"]
Embed["Embed /fetch"]
Wasm["WASM decrypt"]
Proxy["HLS proxy"]
Cdn["Stream CDN"]
Player -->|"ppv.to URL"| Resolve
Resolve --> PpvApi
Resolve --> Embed
Embed --> Wasm
Wasm -->|"streamUrl + embed context"| Player
Player --> Proxy
Proxy --> Cdn
| Step | Action |
|---|---|
| Parse | Full URL, /live/… path, or slug. Strip live/; normalize 24/7- → 247-. |
| Metadata | GET https://api.ppv.to/api/streams/{uri} |
| Source | data.sources entry where default: true |
| Embed context | source.data → { origin, path } from /embed/ URL path |
| Decrypt | resolveEmbedStreamUrl(embed) → CDN index.m3u8 |
| Output | streamUrl, embed, optional proxiedUrl when an origin is supplied |
Errors return { ok: false, stage, error } with stage: input · meta · source · decrypt.
/fetch request
POST {embed.origin}/fetch
Content-Type: application/octet-stream
Origin: {embed.origin}
Referer: {embed.origin}/embed/{embed.path}
Body: 0x0a | u8(path.length) | path UTF-8
/fetch response
- Binary body (protobuf wire payload)
islandheader — session token consumed by WASM
Protobuf decode (slugFromFetchBody)
- Walk length-delimited fields; field 2 string that does not start with
{→ stream slug
WASM execution (src/embed/wasm/gasm.js, gasm.wasm)
- happy-dom
Windowat embed page URL - Mock
jwplayer,P2PEngineHls,fetchreturning the/fetchbody +island - Patch WASM linear memory at fixed offsets
set_stream_jw(island, body)→ scrape memory forhttps://…/secure/…/index.m3u8- Slug match when multiple URLs present in memory
Required because CDN requests need embed headers and segments arrive PNG-wrapped.
| Stage | Behavior |
|---|---|
| Upstream | impit fetch with embed Origin / Referer; 120s timeout for multi-MB segments |
| Playlist | Detect #EXTM3U; rewrite media URIs to proxy URLs; pass through HLS tags |
| Live sync | Drop last segment only when EXTINF < 95% of TARGETDURATION |
| Segments | Strip PNG (IEND boundary or TS 0x47 scan) → video/mp2t |
| Guard | Reject poison playlists (image-only or no media URIs) |
Typical resolved structure: master index.m3u8 → variant tracks-v1a1/mono.ts.m3u8 → CDN segment objects.
import { resolveStream } from './src/stream/resolve.js'
const result = await resolveStream(
'https://ppv.to/live/your-event',
'https://your-api.example' // origin for proxied URL generation; omit if not needed
)On success you receive:
{
"ok": true,
"uri": "your-event",
"contentPath": "/live/your-event",
"embed": { "origin": "https://embedhost.example", "path": "your-event" },
"streamUrl": "https://cdn.example/secure/…/index.m3u8",
"proxiedUrl": "https://your-api.example/api/hls?url=…&embed=…&embedOrigin=…"
}streamUrl— direct CDN master playlist (token-bound, short-lived)embed— required for all upstream CDN requests (referer contract)proxiedUrl— entry point when your stack uses the reference proxy routes
Option A — Reference proxy (recommended pattern)
Point your HLS player at proxiedUrl. The proxy handles referer headers, manifest rewriting, PNG unwrap, and CORS response headers. Implement GET /api/hls using writeProxyHlsResponse from src/hls/proxy.js on your backend.
Option B — Direct CDN (server-side only)
Fetch streamUrl and child playlists/segments from your backend with upstreamFetch(url, embed) from src/embed/upstream.js. Apply rewriteM3u8 and segmentBody logic before forwarding to the client. Never expose raw CDN URLs to a browser without the embed referer chain.
Player requirements
- HLS client (hls.js, Video.js, native Safari HLS, etc.)
- Live tuning for short sliding-window playlists (~4×4s segments): low
liveSyncDurationCount, generousfragLoadingTimeOut(segments are multi-MB) - Backend proxy for any browser-based player
GET /api/hls?url={upstreamUrl}&embed={path}&embedOrigin={origin}
| Response | Content-Type | Body |
|---|---|---|
| Playlist | application/vnd.apple.mpegurl |
Rewritten m3u8 |
| Segment | video/mp2t |
Stripped MPEG-TS |
Shipped with the demo server (src/http/router.js) for pipeline verification.
{ "url": "https://ppv.to/live/your-event" }| Param | Required |
|---|---|
url |
Upstream m3u8 or segment URL |
embed |
From resolve embed.path |
embedOrigin |
From resolve embed.origin |
https://ppv.to/live/{slug}
https://ppv.to/live/247-{channel}
/live/{slug}
{slug}
src/
server.js Demo HTTP entry
config.js API_BASE, USER_AGENT
http/router.js Reference routes
stream/resolve.js Resolve orchestration
embed/
context.js Embed URL parse, proxy URL builder
decrypt.js /fetch, WASM, m3u8 extraction
upstream.js CDN client (impit + referer headers)
media.js Playlist sniff, poison detect
wasm/ gasm.js + gasm.wasm
hls/proxy.js Manifest rewrite, live sync, TS strip
public/
index.html Dev verification UI only
| Layer | Technology |
|---|---|
| Runtime | Node.js 18+ ES modules |
| ppv.to / embed HTTP | Native fetch |
| WASM sandbox | happy-dom |
| Decrypt module | Bundled gasm WASM (set_stream_jw) |
| CDN upstream | impit (Chrome TLS fingerprint) |
| Manifest / segment | Custom m3u8 rewrite, PNG → MPEG-TS |
happy-dom ^20.0.10 · impit ^0.14.1
The demo server is for local pipeline testing only.
npm install
npm start # http://127.0.0.1:8788 — HOST / PORT env overridescurl -s -X POST http://127.0.0.1:8788/api/stream \
-H 'Content-Type: application/json' \
-d '{"url":"https://ppv.to/live/your-event"}'Minimal page used to confirm resolve → proxy → hls.js playback during development. Not part of the integration surface — wire resolveStream and writeProxyHlsResponse into your own stack instead.
| Variable | Default |
|---|---|
HOST |
127.0.0.1 |
PORT |
8788 |
Set X-Forwarded-Proto behind a reverse proxy so generated proxiedUrl values use https://.
This repository documents stream-resolution and playback-integration techniques discovered through independent analysis.
- No affiliation — This project is not affiliated with, endorsed by, or sponsored by ppv.to, its embed providers, CDNs, or rights holders.
- Your responsibility — You are solely responsible for how you use this code. Comply with applicable laws, platform terms of service, and content licensing in your jurisdiction. Do not use this project to circumvent access controls, redistribute copyrighted streams, or violate third-party rights.
- No warranty — The software is provided as-is, without warranty of any kind. Functionality may break without notice if upstream APIs, embed hosts, or protection schemes change.
- Trademarks — Third-party names and marks belong to their respective owners. References are for identification only.
If you are a rights holder and believe this repository infringes your rights, please open an issue or contact the maintainer.