Self-hosted embed scraper and HLS stream resolver for Ployan player pages. Paste a watch URL, scrape the page, reverse engineer the AES-256-GCM session token, and resolve a direct M3U8 master playlist—pure Node.js HTTP and crypto, with no browser automation and no VM.
Built for people who need a working stream link, and for developers studying web scraping, playlist resolution, and player reverse engineering.
- Overview
- Who this is for
- What you get
- Quick start
- Architecture
- How resolve works
- Why a proxy is required in the browser
- HLS proxy
- Project layout
- Stack and requirements
- Disclaimer
A Ployan-powered watch page is not the stream. It is HTML that exposes a media id and a base64 player origin (plyURL). The real HLS playlist stays behind a sealed token on the player host. This project is an end-to-end stream resolver: it scrapes that page, rebuilds the same token the player would seal, calls /get, and returns {origin}/hls/{info}/master.m3u8.
Everything runs server-side. The small web UI only submits a URL, shows timing, plays through a local HLS proxy, and offers copy commands for VLC and MPV.
Example input page: flixhqz.com/movie/scream-7-1630860919/
- You want a direct M3U8 link from an embed page without opening DevTools.
- You want in-browser playback, or a one-click command for VLC / MPV.
- You want a self-hosted tool, not a third-party streaming site.
- You are learning web scraping of embed HTML (media id, player origin, title).
- You are studying reverse engineering of obfuscated player crypto (PBKDF2 + AES-256-GCM).
- You need a clear stream resolver pattern: scrape → seal →
/get→ playlist URL → optional proxy.
- Embed scraper — one HTTP GET on the watch page; reads
#middata-id, decodesplyURL, parses<title>. - Stream resolver — seals a fresh session token and resolves a direct HLS master playlist when the player returns
mode: "direct". - Reverse engineering port — the proven player seal in
node:crypto(no headless browser, no JS VM). - HLS proxy — rewrites
#EXTM3Uplaylists so the browser can load segments through your host. - Local UI — resolve timing, first-frame timing, hls.js playback, export fields for direct URL / proxy / VLC / MPV.
There is one resolve path. Missing fields or a non-direct /get response fail loudly—no alternate scrapers, no native-HLS fallback branch, no silent defaults.
- Node.js ≥ 22.18
- Network reachability to the embed site and the Ployan player origin
npm install
npm startOpen the printed local URL (default http://localhost:3000/).
- Paste a watch / embed page URL.
- Click Resolve.
- Play in the page, or copy the direct M3U8 / VLC / MPV fields.
Optional: set PORT to change the listen port.
PORT=8080 npm startThree network layers sit between a watch URL and playable media:
| Layer | Role |
|---|---|
| Embed site | Serves HTML; exposes mediaId and base64 plyURL |
| Player API | Accepts sealed token on /get/{token}; returns info |
| CDN | Serves media playlists and .ts segments named inside M3U8 |
flowchart LR
A[Watch / embed URL] --> B[Scrape HTML]
B --> C[Decode plyURL]
B --> D[Read mediaId]
C --> E[Seal AES-256-GCM token]
D --> E
E --> F["GET /get/{token}"]
F --> G[master.m3u8]
G --> H[UI proxy / VLC / MPV]
Resolve is pure Node: native fetch for HTML and JSON, node:crypto for the seal. The UI never runs the player bundle.
src/scraper/video.ts performs web scraping with a single GET and a page-origin Referer.
| Field | Source |
|---|---|
mediaId |
id=mid … data-id= |
playerOrigin |
Base64-decoded const plyURL = "…" |
title |
<title> text before | |
If any required field is missing, scrape throws. No slug heuristics, no alternate DOM selectors.
The player never puts the playlist URL in HTML. It seals a short plaintext and sends it to /get. This repo reimplements that seal after reverse engineering the player:
Plaintext
{mediaId}+1+1+{unixTimestamp}
Key derivation
- Password:
player - PBKDF2-SHA256, 1000 iterations
- Salt: 8 random bytes
- Key: 256 bits
AES-256-GCM
- IV: 12 random bytes
- Auth tag: 16 bytes, appended to ciphertext
Wire format
{saltHex}-{ivHex}-{ciphertextHex}{tagHex}
Implementation: src/resolver/seal.ts. Generate a new token per resolve.
src/resolver/playlist.ts:
GET {playerOrigin}/get/{token}withReferer: {playerOrigin}/- Require HTTP 200,
mode: "direct", and a non-emptyinfo - Return
{playerOrigin}/hls/{info}/master.m3u8
That master playlist is the stream resolver output. VLC and MPV can open it directly. The UI plays it through the local proxy.
Orchestration for the UI lives in src/resolver/api.ts → src/server.ts (GET /api/stream).
- CORS / headers — CDN and player hosts often reject browser fetches from your UI origin.
- Relative playlist URLs — media lines inside M3U8 must be rewritten to stay on your host, or the player cannot load the next playlist and segments.
External players (VLC, MPV) talk to the CDN themselves, so they can use the direct M3U8 from the resolver without this server’s proxy.
src/proxy/stream.ts serves GET /api/proxy?url={absolute-url}.
- Fetch upstream with
Referer: {upstream-origin}/. - If the body starts with
#EXTM3U, rewrite media lines andURI="…"values to{your-host}/api/proxy?url=…. - Otherwise pass bytes through (segments, keys, etc.).
Playlist detection is the #EXTM3U magic only—one definition of an HLS playlist body.
| Path | Responsibility |
|---|---|
src/server.ts |
HTTP server, static UI, /api/stream, /api/proxy |
src/config.ts |
Port and upstream request headers |
src/upstream.ts |
fetchText / fetchJson / fetchBytes |
src/scraper/video.ts |
Embed page scraper |
src/resolver/seal.ts |
AES-256-GCM session token seal |
src/resolver/playlist.ts |
/get → M3U8 URL |
src/resolver/api.ts |
UI resolve handler |
src/proxy/stream.ts |
HLS playlist rewrite proxy |
src/ui/ |
HTML, CSS, TypeScript player UI (hls.js) |
Build: npm start runs tsc, copies UI assets and hls.js into dist/, then starts node dist/server.js.
| Area | Choice |
|---|---|
| Runtime | Node.js ≥ 22.18, ES modules |
| Language | TypeScript → dist/ (ESNext) |
| Crypto | node:crypto — pbkdf2Sync, createCipheriv("aes-256-gcm") |
| HTTP | node:http, native fetch |
| Browser playback | hls.js (npm, served from dist/ui/vendor) |
| Variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
Listen port |
npm start
npm run typecheckThis project does not host, store, or distribute media. Embed sites and Ployan player hosts are independent services. The scraper reads public HTML; the resolver seals the same session token shape the player uses and calls /get over HTTP.
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.