live-sfu-demo/README.md

152 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 直播 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。