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, 云资产清单, 代理转发, 流媒体解析, 自定义脚本, 逆向工程