sharoon7171/streamed-pk-hls-stream-resolver

GitHub: sharoon7171/streamed-pk-hls-stream-resolver

将 streamed.pk / embed.st 的加密 HLS 流在本地还原为可播放的 M3U8 并提供带 Referer 伪装的中继服务。

Stars: 0 | Forks: 0

# Streamed.pk HLS 流解析器 这是一个本地服务器,用于将 [streamed.pk](https://streamed.pk) 或 [embed.st](https://embed.st) 的流 URL 转换为可播放的 HLS 播放列表。它在 Node 中重现了 embed.st 的客户端握手过程,使用 `lock.wasm` 解密上游 M3U8,并在 CDN 拒绝直接请求时提供 HLS 中继。 需要 Node.js ≥ 22,并且 `curl` 需要在 PATH 中。 ## 目录 - [概述](#overview) - [快速开始](#quick-start) - [接受的输入 URL](#accepted-input-urls) - [架构](#architecture) - [Embed 握手与 GOAT 解密](#embed-handshake-and-goat-decrypt) - [HLS 中继](#hls-relay) - [播放](#playback) - [技术栈](#stack) - [HTTP API](#http-api) - [配置](#configuration) - [项目结构](#project-layout) - [范围与限制](#scope-and-limits) - [免责声明](#disclaimer) ## 概述 streamed.pk 的观看页面本身并不是流。它指向了一个 **embed.st** 播放器。**HLS 播放列表 URL 从未出现在 HTML 中** —— embed 会发送一个 protobuf `POST /fetch` 请求,在 WASM 中解密响应,然后才会请求 CDN 的 `.m3u8` 文件。 这里涉及三个来源: | 层级 | 作用 | | --- | --- | | streamed.pk | 比赛元数据和流链接查找(仅限观看 URL) | | embed.st | `/fetch` 握手,`goat` header,WASM 解密 | | CDN (`strmd.st`, tiktokcdn) | HLS 播放列表和 MPEG-TS 分片 | 本项目在服务端重现了这一链路,并通过 `POST /api/stream`、`GET /api/hls` 以及浏览器 UI 将其暴露出来。 ## 快速开始 ``` npm install npm start ``` 打开 `http://localhost:3000`,粘贴流 URL,然后点击 **Resolve**。 默认端口为 `3000`。可以通过 `PORT` 覆盖,或使用 `HOST` 指定绑定地址。 ## 接受的输入 URL | 形式 | 示例 | | --- | --- | | Streamed.pk 观看页面 | `https://streamed.pk/watch/leinster-vs-bulls-2483276/admin/1` | | Streamed.pk 流 API | `https://streamed.pk/api/stream/admin/ppv-leinster-vs-bulls?stream=1` | | 直接 embed.st URL | `https://embed.st/embed/admin/ppv-leinster-vs-bulls/1` | 观看 URL 必须包含 `{source}/{stream}`(例如 `/admin/1`)。较短的 `/watch/{matchId}` 路径会被拒绝。 ### 通过 API 构建观看 URL | 部分 | 来源 API | 示例 | | --- | --- | --- | | `matchId` | `/api/matches/all` 中的 `match.id` | `leinster-vs-bulls-2483276` | | `source` | `match.sources[].source` | `admin` | | `stream` | `/api/stream/{source}/{source.id}` 中的 `streamNo` | `1` | 完整 URL:`https://streamed.pk/watch/{matchId}/{source}/{stream}` 解析逻辑:`src/goat/parse.js`。观看 URL 会调用 `src/streamed/` 在执行 `/fetch` 之前解析 embed 插槽。 ## 架构 解析过程由来自 UI 的 `POST /api/stream` 触发。编排逻辑位于:`src/goat/run.js`。 ``` sequenceDiagram participant UI as player.js participant Route as router.js participant Run as goat/run.js participant SPK as streamed.pk participant EST as embed.st participant WASM as lock.wasm participant Relay as relay/m3u8.js participant CDN as HLS CDN UI->>Route: POST /api/stream { url } Route->>Run: run(input, origin) alt watch URL Run->>SPK: match + stream lookup SPK-->>Run: embed slot else embed URL Run-->>Run: embed slot from URL end Run->>EST: POST /fetch (protobuf body) EST-->>Run: goat header + encrypted body Run->>WASM: decrypt in worker thread WASM-->>Run: m3u8 URL Run-->>UI: { m3u8, relay, … } UI->>Relay: GET relay (hls.js) Relay->>CDN: curl with embed Referer CDN-->>Relay: playlist or segment Relay-->>UI: rewritten m3u8 or stripped TS ``` | 层级 | 模块 | 职责 | | --- | --- | --- | | HTTP | `src/http/router.js` | `/api/stream`,`/api/hls`,静态资源 | | 解析 | `src/goat/run.js` | 解析 → 获取 → 解密 → 中继链接 | | 解析 | `src/goat/parse.js` | 观看、embed 和 API URL → embed 插槽 | | 比赛查找 | `src/streamed/` | 用于观看 URL 的 streamed.pk API | | 加密 | `src/goat/lock-worker.js` | 通过 worker 中的 `lock.wasm` 进行 GOAT 解密 | | 网络 | `src/wire/embed.js`,`src/wire/curl.js` | Embed POST;带 referer 的 CDN 拉取 | | 中继 | `src/relay/m3u8.js`,`src/relay/segment.js` | M3U8 重写;PNG 封装的 TS 剥离 | | UI | `public/player.js` | 解析表单,hls.js,VLC/MPV 导出 | 握手与 WASM 细节:[Embed 握手与 GOAT 解密](#embed-handshake-and-goat-decrypt)。中继与播放:[HLS 中继](#hls-relay),[播放](#playback)。 ## Embed 握手与 GOAT 解密 ### Embed 插槽 每个解析路径最终都会得到一个用于 `/fetch`、WASM 以及中继 referer header 的 embed 插槽: ``` { origin: "https://embed.st", path: "admin/ppv-leinster-vs-bulls/1", source, id, stream, slug } ``` 由 `src/goat/parse.js`(直接 embed URL)或 `src/streamed/watch.js`(观看 URL)构建。 ### `/fetch` 请求 `src/goat/proto.js` 将三个 protobuf 字符串字段 —— `source`、`id`、`stream` —— 编码进 POST 请求体中。 `src/wire/embed.js` 发送: ``` POST {origin}/fetch Content-Type: application/octet-stream Origin: {origin} Referer: {origin}/embed/{path} ``` ### `/fetch` 响应 | 部分 | 用途 | | --- | --- | | 请求体 | 加密的数据块;WASM 将其解密以恢复播放列表 URL | | `goat` header | 传入 WASM 的 32 字符密钥材料(例如 `NOSCRPS…`) | ### WASM 解密 `src/goat/lock.js` 在一个 **worker 线程** 中启动 `src/goat/lock-worker.js`。该 worker 会: - 挂载一个带有 stub 后的 `jwplayer` 和 mock `fetch` 的 **happy-dom** window - 通过 `lock-esm.mjs` 加载 `src/goat/vendor/lock.wasm` - 调用 `set_stream_jw(source, id, stream)`;WASM 解密请求体并在内部请求 `.m3u8` - 返回捕获到的 CDN URL,例如 `https://lb10.strmd.st/secure/…/high/mono.m3u8` WASM 之所以在 worker 中运行,是因为它会修改全局的 `fetch` —— 如果在主线程上运行,会破坏后续的 API 调用。 解析输出:原始的 **`m3u8`** 以及一个指向本服务器 `/api/hls` 的 **`relay`** URL。 ## HLS 中继 浏览器和大多数播放器无法直接获取 `strmd.st` —— CDN 会检查 embed `Referer` 并拦截直接请求。解析完成后会返回两个 URL: | URL | 含义 | | --- | --- | | `m3u8` | 直接上游播放列表 | | `relay` | 通过本服务器的 `GET /api/hls` 获取的相同内容 | `src/relay/m3u8.js` 处理 `GET /api/hls`: 1. **拉取上游**,通过 `src/wire/curl.js` 发送 `Referer: {embedOrigin}/` 和 `Origin: {embedOrigin}`。 2. **播放列表** —— 检测 `#EXTM3U`,将每一行媒体行和 `URI="…"` 标签重写回 `/api/hls`。 3. **分片** —— tiktokcdn 返回封装在 PNG 中的 MPEG-TS;`src/relay/segment.js` 剥离封装层并返回 `video/mp2t`。 查询参数: | 参数 | 是否必需 | 描述 | | --- | --- | --- | | `url` | 是 | 上游播放列表或分片 URL | | `embed` | 是 | Embed 路径,例如 `admin/ppv-leinster-vs-bulls/1` | | `embedOrigin` | 是 | Embed 主机,例如 `https://embed.st` | 在浏览器、VLC 和 MPV 中请使用 **`relay`**。**`m3u8`** 适合用于调试,但如果缺少 referer 通常会被拦截。 ## 播放 ### 在浏览器中 UI 从 jsDelivr 加载 **hls.js** 1.5.20 并播放 **`relay`**,这样 referer 的处理就保留在了服务器端。 ### VLC / MPV UI 会复制使用代理 URL 的命令 —— 不需要 referer: ``` vlc "http://localhost:3000/api/hls?url=…&embed=…&embedOrigin=…" mpv --force-media-title="Leinster vs Bulls" "http://localhost:3000/api/hls?url=…&embed=…&embedOrigin=…" ``` 如果播放器会发送 embed referer,你也可以直接打开 **`m3u8`**;但使用代理 URL 会更简单。 ## 技术栈 | 角色 | 技术 | | --- | --- | | 运行时 | Node.js ≥ 22,ES modules,原生 `fetch` | | HTTP | `node:http` | | Embed WASM 沙盒 | `happy-dom` + `worker_threads` | | WASM 打包 | `lock.wasm`,`lock-esm.mjs`(vendor 打包使用 `big-integer`) | | 上游 CDN 拉取 | `curl`(referer header;strmd.st 上的 Node fetch 会被拦截) | | 分片解包 | `relay/segment.js` —— 来自 tiktokcdn 的 PNG 封装 MPEG-TS | | 浏览器 HLS (UI) | 来自 jsDelivr 的 hls.js 1.5.20 | 不需要浏览器、Playwright 或无头 Chrome。 ## HTTP API ### `POST /api/stream` 通过观看或 embed URL: ``` { "url": "https://streamed.pk/watch/leinster-vs-bulls-2483276/admin/1" } ``` 程序化方式(根据 [streamed.pk API](https://streamed.pk/docs) 进行验证) —— `source` 和 `stream` 是必填项: ``` { "matchId": "leinster-vs-bulls-2483276", "source": "admin", "stream": 1 } ``` 成功: ``` { "ok": true, "matchId": "leinster-vs-bulls-2483276", "title": "Leinster vs Bulls", "slug": "ppv-leinster-vs-bulls", "source": "admin", "stream": "1", "watchUrl": "https://streamed.pk/watch/leinster-vs-bulls-2483276/admin/1", "embedUrl": "https://embed.st/embed/admin/ppv-leinster-vs-bulls/1", "m3u8": "https://lb….strmd.st/secure/…/high/mono.m3u8", "relay": "http://localhost:3000/api/hls?url=…&embed=…&embedOrigin=…" } ``` 失败: ``` { "ok": false, "stage": "input", "error": "match not found: …" } ``` 阶段:`input`(错误的 URL / 缺少比赛)或 `resolve`(获取 / 解密 / 上游失败)。 ### `GET /api/hls` HLS 中继。查询参数请参见 [HLS 中继](#hls-relay)。 ### 静态 UI `/` 提供 `public/index.html`、`player.js` 和 `style.css`。 ## 配置 环境变量(`src/env.js`): | 变量 | 默认值 | 用途 | | --- | --- | --- | | `PORT` | `3000` | 监听端口 | | `HOST` | 所有网络接口 | 设置时的绑定地址 | | `STREAMED_ORIGIN` | `https://streamed.pk` | 比赛 API 主机 | | `EMBED_ORIGIN` | `https://embed.st` | Embed 主机 | | `USER_AGENT` | Chrome 149 macOS 字符串 | 出站 fetch User-Agent | ## 项目结构 ``` src/ server.js HTTP entry env.js PORT, origins, USER_AGENT http/router.js /api/stream, /api/hls, static http/static.js public file serving streamed/ streamed.pk match lookup (watch URLs only) goat/run.js resolve + decrypt orchestrator goat/parse.js URL → slot, relay link goat/proto.js protobuf body goat/lock.js spawn WASM worker goat/lock-worker.js GOAT decrypt goat/vendor/ lock.wasm, lock-esm.mjs wire/embed.js POST embed.st/fetch wire/curl.js CDN pull (curl) relay/m3u8.js HLS relay + URL rewrite relay/segment.js PNG-wrapped TS strip public/ index.html resolver UI player.js hls.js player, timers, VLC/MPV export style.css ``` ## 范围与限制 - **仅限 streamed.pk / embed.st** —— 不支持其他来源。 - **直接 `m3u8`** 返回用于检查,但如果缺少中继或 embed referer 可能无法播放。 - **上游 token 会过期** —— 不会进行任何持久化或缓存。 - 观看 URL 的比赛**必须**存在于 `/api/matches/all` 中;如果比赛已结束,请使用直接的 embed URL。 - 获取 CDN 和 strmd.st **需要 `curl`**。 ## 免责声明 仅限**学习与研究**用途 —— 旨在了解 embed.st 客户端握手是如何生成 HLS 播放列表 URL 的。本仓库不拥有、托管或授予任何视频内容的权利。按“原样”提供,不提供任何保证。
标签:AI工具, GNU通用公共许可证, HLS, MITM代理, Node.js, WebAssembly, 云资产清单, 代理转发, 流媒体解析, 自定义脚本, 逆向工程