sharoon7171/watch-footy-hls-stream-resolver

GitHub: sharoon7171/watch-footy-hls-stream-resolver

本地 Node 服务器,通过复现 sportsembed 客户端握手与加密流程,将 Watch Footy 流媒体页面 URL 解析为可播放的 HLS 地址,供协议研究使用。

Stars: 0 | Forks: 0

# Watch Footy HLS 流解析器 这是一个本地服务器,用于将 [watchfooty.st](https://watchfooty.st) 的流媒体页面 URL 转换为可播放的 HLS 播放列表。它会完成 sportsembed 客户端握手,解密上游 M3U8,在 CDN 要求 Referer 时可选择代理分段,并为浏览器、VLC 和 MPV 提供导出链接。 要求 Node.js ≥ 22。运行时无 npm 依赖。 ## 目录 - [概述](#overview) - [快速开始](#quick-start) - [接受的输入 URL](#accepted-input-urls) - [架构](#architecture) - [Embed API 与加密机制](#embed-api-and-cryptography) - [HLS 代理](#hls-proxy) - [播放](#playback) - [HTTP API](#http-api) - [配置](#configuration) - [项目结构](#project-layout) - [范围与限制](#scope-and-limits) - [免责声明](#disclaimer) ## 概述 Watch Footy 嵌入了来自 **sportsembed** 的流(默认源地址为 `https://sportsembed.su`)。嵌入式播放器不会在页面源码中暴露原始的 M3U8。相反,它会: 1. 向 `POST /api/get` 发送一个已签名的 protobuf 请求 2. 接收加密的响应体 3. 在 WebAssembly 中对其进行解密,以获取 HLS 主播放列表或媒体播放列表 URL 本项目在 Node 中重新实现了该客户端路径,以便可以在嵌入的 iframe 之外使用该播放列表。 ## 快速开始 ``` npm start ``` 打开 `http://localhost:3000`,粘贴一个 Watch Footy 直播流 URL,然后点击 **Resolve**。 默认端口为 `3000`。可以通过 `PORT` 覆盖,或使用 `HOST` 设置绑定地址。 ## 接受的输入 URL | 形式 | 示例 | | --- | --- | | 完整的 Watch Footy 路径 | `https://watchfooty.st/pt/stream/4668278/belgium-w-germany-w/regular/2` | | 短路径(从 Watch Footy API 获取的链接) | `https://watchfooty.st/stream/4668278` | | 短路径 + 直播流选择器 | `https://watchfooty.st/stream/4668278?stream=2` | | 直接 sportsembed 嵌入 | `https://sportsembed.su/embed/4668278/belgium-w-germany-w/regular/2` | 解析逻辑位于 `src/resolver/url.js` 中。短 URL 会调用 `api.watchfooty.st` 来解析 slug、分类和流编号,然后再请求 embed API。 ## 架构 解析由 UI 发送的 `POST /api/stream` 触发。编排逻辑位于:`src/resolver/resolve.js`。 ``` sequenceDiagram participant UI as app.js participant Route as route.js participant Resolve as resolve.js participant URL as url.js participant Match as match.js participant WF as watchfooty API participant PB as protobuf.js participant WASM as stream.wasm participant Fetch as fetch.js participant SE as sportsembed participant Proxy as proxy/hls.js participant CDN as HLS CDN UI->>Route: POST /api/stream { url } Route->>Resolve: resolveStream(input, origin) Resolve->>URL: parseStreamUrl(url) alt short Watch Footy URL URL->>Match: fetchMatchLinks(matchId) Match->>WF: sports.getMatchById WF-->>Match: fixture links end URL-->>Resolve: embed descriptor Resolve->>PB: encodeRequestBody(embed) Resolve->>WASM: signRequest(body, nonce) Resolve->>Fetch: fetchEmbedApi(body, headers) Fetch->>SE: POST /api/get SE-->>Fetch: encrypted body, x-live, x-client-factor Resolve->>WASM: decryptStreamUrl(...) Resolve-->>Route: streamUrl, proxiedUrl Route-->>UI: JSON UI->>Proxy: GET proxiedUrl (browser playback) Proxy->>Fetch: fetchCdn(segment, Referer) Fetch->>CDN: GET playlist / .ts ``` | 层级 | 模块 | 职责 | | --- | --- | --- | | HTTP | `src/http/route.js` | 路由 `/api/stream`、`/api/hls`、静态资源 | | 解析器 | `src/resolver/resolve.js` | URL 解析 → 签名 → 获取 → 解密 | | Crypto | `src/resolver/wasm.js` | WASM 签名与解密 | | Network | `src/network/fetch.js` | Embed API POST 及带 Referer 的 CDN GET 请求 | | Proxy | `src/proxy/hls.js` | M3U8 重写与分片中继 | | UI | `public/app.js` | 解析表单、hls.js 播放、VLC/MPV 导出 | Protobuf 字段、WASM 导出和响应头:[Embed API 与加密机制](#embed-api-and-cryptography)。播放选项:[播放](#playback)。 ## Embed API 与加密机制 ### 请求(客户端证明) Embed 端期望接收包含四个 protobuf 字符串字段的 `application/octet-stream` 响应体: | 字段 | 来源 | | --- | --- | | 1 | category(例如 `regular`) | | 2 | slug(例如 `belgium-w-germany-w`) | | 3 | stream number(流编号) | | 4 | match id(比赛 ID) | 编码:`src/resolver/protobuf.js` → `encodeRequestBody()`。 在发送 POST 请求之前,会生成一个 32 字节的随机 **nonce**。`src/resolver/stream.wasm`(由 `src/resolver/wasm.js` 加载)导出了: | 导出项 | 作用 | | --- | --- | | `create_client_factor` | 从请求体和 nonce 派生出 16 字节的 factor | | `sign_proof` | 生成 UTF-8 编码的 proof 字符串 | | `decrypt_payload` | 解密响应密文 | 发送至 `/api/get` 的 Headers: | Header | 内容 | | --- | --- | | `x-client-nonce` | base64(nonce) | | `x-client-factor` | base64(factor) | | `x-client-proof` | proof 字符串 | | `Origin` / `Referer` | embed 源地址和 `/embed/{path}` | 具体实现:`src/network/fetch.js` → `fetchEmbedApi()`。 ### 响应(加密的播放列表 URL) 成功后,响应将包含: | 部分 | 用途 | | --- | --- | | Body | 包含 HLS URL 的加密数据块 | | `x-live` | 密钥材料;`_` 之后的十六进制后缀(32 字节) | | `x-client-factor` | 服务器 factor(base64),与请求 factor 配对用于解密 | 解密输入:加密的响应体、来自 `x-live` 的密钥、相同的 nonce、响应 factor。输出为明文的 M3U8 URL 字符串。 密钥提取:位于 `src/resolver/protobuf.js` 的 `decryptKeyFromHeader()`。 解密调用:位于 `src/resolver/wasm.js` 的 `decryptStreamUrl()`。 WASM 二进制文件是 sportsembed 的客户端 Crypto 模块,在 Node 的 `WebAssembly.instantiate` 下运行——无需浏览器。 ## HLS 代理 上游 CDN 通常会拒绝没有 `Referer: https://sportsembed.su/` 的请求。解析完成后会返回两个 URL: | URL | 含义 | | --- | --- | | `streamUrl` | 直接上游 M3U8 | | `proxiedUrl` | 通过本服务器的 `GET /api/hls` 重写后的同一播放列表 | 代理(`src/proxy/hls.js`)会: - 通过 `fetchCdn()` 携带 embed Referer 请求上游 - 检测播放列表(`#EXTM3U` 或 `application/vnd.apple.mpegurl`) - 将每个分片和嵌套的 `URI="..."` 行重写为通过 `/api/hls` 循环回传 - 原样透传二进制分片(如 `.ts` 等) `/api/hls` 的查询参数: | 参数 | 必需 | 描述 | | --- | --- | --- | | `url` | 是 | 目标分片或播放列表 URL | | `embed` | 是 | Embed 路径 `{matchId}/{slug}/{category}/{stream}` | | `embedOrigin` | 是 | Embed 主机(例如 `https://sportsembed.su`) | ## 播放 ### 浏览器内播放 UI 从 jsDelivr 加载 **hls.js** 并播放 **`proxiedUrl`**,以便将 Referer 处理保留在服务器端。致命的 HLS 错误会显示为用户可见的消息;请使用 VLC/MPV 作为备选。 ### VLC 使用带有明确 Referer 的 **Direct URL**(无需代理): ``` vlc --http-referrer="https://sportsembed.su/" "https://…/playlist.m3u8" ``` 解析完成后,UI 会复制此命令。 ### MPV 具有相同的 Referer 要求: ``` mpv --referrer="https://sportsembed.su/" "https://…/playlist.m3u8" ``` 可选标题: ``` mpv --referrer="https://sportsembed.su/" --force-media-title="Belgium W Germany W" "https://…/playlist.m3u8" ``` ### 在外部播放器中使用代理 URL 你也可以在 VLC 或 MPV 中打开 `proxiedUrl` 而无需设置 Referer——本地服务器会在请求上游时自动添加它: ``` mpv "http://localhost:3000/api/hls?url=…&embed=…&embedOrigin=https://sportsembed.su" ``` 当你不需要运行代理时,首选使用 Direct URL + Referer。 ## HTTP API ### `POST /api/stream` 解析 Watch Footy 或 embed URL。 **请求** ``` { "url": "https://watchfooty.st/pt/stream/4668278/belgium-w-germany-w/regular/2" } ``` **成功** ``` { "ok": true, "slug": "belgium-w-germany-w", "streamUrl": "https://…/master.m3u8", "proxiedUrl": "http://localhost:3000/api/hls?url=…&embed=…&embedOrigin=…" } ``` **失败** ``` { "ok": false, "stage": "input|resolve", "error": "…" } ``` ### `GET /api/hls` HLS 中继。查询参数请参见 [HLS 代理](#hls-proxy)。 ### 静态 UI `/` 提供 `public/index.html`、`app.js` 和 `style.css`。 ## 配置 环境变量(`src/config.js`): | 变量 | 默认值 | 用途 | | --- | --- | --- | | `PORT` | `3000` | 监听端口 | | `HOST` | 所有网络接口 | 设置时的绑定地址 | | `EMBED_ORIGIN` | `https://sportsembed.su` | 用于非 embed Watch Footy URL 的 embed 主机 | | `USER_AGENT` | Chrome 149 macOS 字符串 | 出站请求的 User-Agent | ## 项目结构 ``` src/ server.js HTTP entry config.js env defaults http/route.js /api/stream, /api/hls, static routing http/static.js public file serving resolver/ resolve.js resolve pipeline url.js URL parsing, proxy URL builder protobuf.js request encode, x-live key parse wasm.js WASM load, sign, decrypt stream.wasm sportsembed client crypto (required) network/fetch.js embed API + CDN fetch proxy/hls.js playlist rewrite + segment relay watchfooty/match.js TRPC match links for short URLs public/ index.html resolver UI app.js hls.js player, VLC/MPV export lines style.css ``` ## 范围与限制 - **仅限 Watch Footy / sportsembed。** 其他聚合器(例如 StreamNinja)使用不同的后端,不在本项目范围内。 - **实时握手。** 当直播流离线或受地域限制时,`/api/get` 可能会失败;错误将直接从 embed API 透传。 - **绑定 Referer 的 CDN。** 在未发送 sportsembed Referer 的播放器中,直接的 `streamUrl` 会失败;请使用 `proxiedUrl` 或复制的 VLC/MPV 命令。 - **无持久化。** 解析出的 URL 仅在当前会话有效;上游 token 可能会过期。 ## 免责声明 仅供**学习与研究**——了解 sportsembed 嵌入式客户端握手是如何生成 HLS 播放列表 URL 的。本代码库不拥有、托管或授予任何视频、广播或其他受版权保护材料的权利。 在测试任何内容前,你必须已经拥有合法的访问权限。作者不对第三方内容主张版权,也不对滥用行为承担责任。按“原样”提供,不提供任何保证。
标签:AI工具, GNU通用公共许可证, HLS代理, MITM代理, Node.js, WASM逆向, 协议分析, 权限提升, 流媒体解析, 自定义脚本, 音视频工具