Skip to content

sharoon7171/ppv-hls-stream-resolver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PPV HLS Stream Resolver

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/…

Table of contents

Introduction

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]
Loading

This repository reproduces that chain server-side and exposes it via POST /api/stream, GET /api/hls, and a browser UI.

What gets resolved

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.

How decryption works

Embed source

src/embed/context.js parses the default source from the streams API:

https://{embed-host}/embed/{path}

{ origin: "https://{embed-host}", path: "{path}" }

/fetch handshake

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.

WASM (gasm.wasm)

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.

How the stream is resolved

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.

MetadataGET {API_BASE}/streams/{uri} (API_BASE is https://api.ppv.to/api in src/env.js). Reads the default entry from data.sources.

DecryptresolveEmbedStreamUrl in src/embed/decrypt.js runs the /fetch + WASM chain above.

Proxied URLrelayUrl 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.

Why it cannot play directly in the browser

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.

How the relay works

src/relay/hls.js serves GET /api/hls.

  1. Fetch upstream via impit (src/embed/upstream.js) with Referer: {origin}/embed/{path} and Origin: {origin}.
  2. Validate body — reject HTML error pages, empty payloads, and poison playlists (src/embed/media.js).
  3. M3U8 — rewrite media lines and URI="…" tags in src/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.
  4. Segments — strip non-TS prefix bytes (e.g. PNG wrapper) in src/relay/segment.js, then return video/mp2t.

The browser only talks to your host. VLC/MPV can skip the relay and open the direct M3U8.

Stack

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 start

Startup logs the listen URL from srv.address() (e.g. http://localhost:3000/).

Code map

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

REST API

POST /api/stream

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: *.

GET /api/hls

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 url400. Upstream failure → 502 plain text.

Static

Path File
/ public/index.html
/css/app.css styles
/js/app.js UI

Disclaimer

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.

About

PPV HLS Stream Resolver — fetch ppv.to stream metadata, replay embed /fetch protobuf handshake, WASM decrypt to CDN m3u8, proxy HLS playback

Topics

Resources

Stars

Watchers

Forks

Contributors