152 lines
6.6 KiB
Markdown
152 lines
6.6 KiB
Markdown
# 直播 SFU 分流 Demo(Cloudflare Realtime · SRS)
|
||
|
||
参考 GOSpeak 技术栈的直播 SFU 分流演示。控制面用 **protobuf / gRPC** 定义,媒体面走
|
||
**Cloudflare Realtime**(主 SFU)与 **SRS**(本地对照)的 WebRTC 扇出。
|
||
|
||
- 一路推流 → SFU 扇出 → 多路拉流。
|
||
- 同一路发布可同时落到 **Cloudflare Realtime** 与 **SRS** 两条分发链路(分流)。
|
||
- 浏览器经服务端反向代理直连 SFU,凭证不出服务端。
|
||
- 默认 DB 使用 Turso 嵌入式 (libSQL file):房间分发拓扑持久化到本地文件。
|
||
- 前端为 **SolidJS + TanStack Router/Query + Ark UI + Tailwind v4** 单页应用,构建产物嵌入 Go 二进制。
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
live-sfu-demo/
|
||
api/live_sfu.proto # protobuf 控制面契约
|
||
gen/ # buf generate 产出(Go)
|
||
internal/
|
||
config/ # 运行参数
|
||
db/ # Turso 嵌入式 (libSQL file) 持久化
|
||
sfu/cloudflare/ # Cloudflare Realtime REST 客户端 + Provider
|
||
sfu/srs/ # SRS Provider
|
||
server/ # gRPC 服务 + JSON 网关 + SRS/CF 媒体反代 + 房间扇出状态
|
||
static/ # 前端构建产物(由 web/ 构建生成,Go embed)
|
||
auth/ # Casbin RBAC + JWT + bcrypt 用户存储
|
||
cmd/server/main.go
|
||
deploy/ # docker-compose.yml + srs.conf
|
||
web/ # 前端工程(Solid + TanStack + Ark UI + Tailwind v4)
|
||
src/ # 页面 / 组件 / 库
|
||
vite.config.ts # 构建输出到 ../internal/server/static
|
||
```
|
||
|
||
## 运行(SRS 链路,开箱即跑)
|
||
|
||
```bash
|
||
# 1) 起本地 SRS(WHIP/WHEP/HLS 三协议后端)
|
||
cd deploy && SRS_CANDIDATE=127.0.0.1 docker compose up -d srs
|
||
|
||
# 2) 起 Demo 控制面(默认 Turso 嵌入式,无需额外配置)
|
||
cd live-sfu-demo
|
||
cp .env.example .env
|
||
go run ./cmd/server
|
||
|
||
# 3) 打开浏览器(发布/订阅需先登录,见下方鉴权说明)
|
||
# 首页: http://localhost:8088/
|
||
# 发布: http://localhost:8088/publish?room=demo
|
||
# 观看: http://localhost:8088/watch?room=demo
|
||
```
|
||
|
||
两个标签页用同一房间名即可配对。发布页勾选的分发后端会在「分发状态」中实时显示;
|
||
SRS 链路同时产出 HLS/FLV,分发状态面板可直接获取 `/live/<stream>.m3u8` / `.flv` 链接,
|
||
经 Go 网关 (`/live/*`) 反代,无需浏览器直连 8080。
|
||
|
||
## 鉴权(Casbin RBAC + JWT)
|
||
|
||
控制面已启用登录与 RBAC:
|
||
|
||
- 发布 / 订阅 / 房间管理需要登录;`guest` 仅可读取配置与 SRS 切片流(HLS/FLV)。
|
||
- 默认账号(bcrypt,落盘 `data/users.json`):
|
||
- `admin / Admin123!`(admin)
|
||
- `publisher / Publisher123!`(publisher)
|
||
- `viewer / Viewer123!`(viewer)
|
||
- 登录后服务端下发 HttpOnly Cookie,前端同源请求自动携带;在 `/login` 登录。
|
||
|
||
## 默认 DB:Turso 嵌入式
|
||
|
||
房间“分发目标”拓扑(`stream_targets`)默认持久化到 **Turso 嵌入式**
|
||
(`github.com/tursodatabase/go-libsql` 的 SQLite 兼容 file 模式),而非纯内存。
|
||
|
||
- **DSN**:`TURSO_DATABASE_URL=file:./data/live-sfu.db?cache=shared&_journal_mode=WAL`(默认)
|
||
- 兼容 `DATABASE_URL` 覆盖
|
||
- 仅支持嵌入 file: / :memory:,拒绝 libsql:// 远程(嵌入适配专注单机持久化)
|
||
- 自动建表 `(stream_targets, rooms)`,`SetMaxOpenConns(1)` 适配 SQLite 单写模型
|
||
- 重启后自动 `LoadAll` 恢复房间
|
||
|
||
```bash
|
||
# 默认即嵌入文件
|
||
TURSO_DATABASE_URL=file:./data/live-sfu.db?cache=shared&_journal_mode=WAL
|
||
|
||
# 验证持久化:发布后重启,/api/rooms 仍在
|
||
curl -b cookie.txt http://localhost:8088/api/rooms | jq
|
||
sqlite3 data/live-sfu.db "select room, backend, stream from stream_targets;"
|
||
```
|
||
|
||
## 运行(Cloudflare Realtime 链路,主 SFU)
|
||
|
||
在 `.env` 填入 Cloudflare Realtime 凭证后,`go run ./cmd/server` 即启用主 SFU:
|
||
|
||
```
|
||
CF_APP_ID=xxxxxxxxxxxx
|
||
CF_APP_SECRET=xxxxxxxxxxxx
|
||
```
|
||
|
||
- 发布:服务端用 `CF_APP_SECRET` 创建 Cloudflare session,浏览器只交换 SDP(`/api/cf/...` 反代注入 Bearer)。
|
||
- 观看:服务端为观众创建 viewer session,并订阅发布者 session 的轨道(`location=remote`)。
|
||
|
||
Cloudflare 未配置时,UI 会标注「未配置」,SRS 链路不受影响。
|
||
|
||
## protobuf / gRPC
|
||
|
||
```bash
|
||
# 修改 api/live_sfu.proto 后重新生成
|
||
export PATH="$HOME/go/bin:$PATH"
|
||
buf generate api
|
||
```
|
||
|
||
契约(`LiveSFU` 服务):`GetConfig` / `ListRooms` / `Publish` / `Subscribe` / `StopStream` /
|
||
`WatchRoom`(服务端流式推送房间分发拓扑)以及 `Login` / `Register` / `GetMe` / `ListUsers` / `UpdateUserRole`。
|
||
gRPC 监听 `GRPC_PORT`(默认 9090),浏览器走同端口的 JSON 网关(`protojson`)。
|
||
|
||
## 前端开发(web/)
|
||
|
||
```bash
|
||
cd web
|
||
pnpm install
|
||
pnpm dev # http://localhost:5173 热更新
|
||
pnpm build # 构建进 ../internal/server/static(被 Go embed)
|
||
```
|
||
|
||
技术栈:SolidJS(`vite-plugin-solid`)、TanStack Router + Query、Ark UI(无样式原语)、
|
||
Tailwind v4(CSS-first `@theme` 做 design token 体系化驱动)。design token 集中在
|
||
`web/src/index.css` 的 `@theme`,组件语义类在 `web/src/components/ui.tsx` 中以 token 派生。
|
||
|
||
## 分流拓扑
|
||
|
||
```
|
||
publisher ──WHIP/tracks.new──▶ Cloudflare Realtime SFU ──▶ viewer(s)
|
||
└────WHIP────────────▶ SRS SFU ─┬─▶ WHEP viewer(s) (低延时 0.2-0.5s)
|
||
├─▶ HLS viewer(s) (全端兼容,/live/*.m3u8)
|
||
└─▶ FLV viewer(s)
|
||
一路 WHIP 推流,SRS 自动 remux 三协议同出;房间分发目标由 WatchRoom 实时广播
|
||
```
|
||
|
||
> SRS 推流链路详解见 [`docs/streaming-pipeline.md`](docs/streaming-pipeline.md)。
|
||
|
||
## 主播外部推送 API(弹幕广播)
|
||
|
||
除观众可发送的 `POST /api/room/{room}/chat` 外,额外提供**仅主播可用**的独立推送端点,供 OBS 脚本 / 机器人 / 管理工具以主播身份推送弹幕:
|
||
|
||
```
|
||
POST /api/room/{room}/broadcast
|
||
Authorization: Bearer <publisher-or-admin jwt>
|
||
Content-Type: application/json
|
||
|
||
{ "message": "欢迎来到直播间", "color": "#4ea8ff" }
|
||
```
|
||
|
||
- 权限:需 `room:publish`(仅 `publisher` / `admin`),未登录返回 `401`,越权返回 `403`。
|
||
- 推送的弹幕自动标记 `host: true`,经同一 `chatHub` 广播,主播端与观众端实时可见;前端在消息列表与弹幕叠加层以「主播」标识区分。
|
||
- 同样受滑动窗口限流(房间 + 发送者 + IP,3s 内 5 条)保护。
|
||
- 外部工具用 `Authorization: Bearer <token>` 调用;浏览器端同源请求自动携带登录 Cookie。
|