- roomHub: add globalSubs + subscribeRooms/broadcastRooms, emit create/update/delete on createRoom/setTarget/removeTarget/setDistribution/clearDistributions/removeRoom - gateway: handleRoomsEvents SSE (event: rooms, sync + 25s ping) - server: GET /api/rooms/events (room:list) - sse.ts: subscribeRooms with onState (onopen/onerror) - rooms.tsx: subscribe + 350ms debounce reload, live badge + lastSync - config_test: align ValidateDistributors permissive semantics (cdn占位不阻断) - docs: document /api/rooms/events in business-logic & design |
||
|---|---|---|
| api | ||
| cmd/server | ||
| data | ||
| deploy | ||
| docs | ||
| gen | ||
| internal | ||
| web | ||
| .env.example | ||
| .gitignore | ||
| Makefile | ||
| README.md | ||
| buf.gen.yaml | ||
| design.md | ||
| go.mod | ||
| go.sum | ||
README.md
SyncLive — 同步直播 · SFU 分流服务(对标 SyncTV)
对标 SyncTV:SyncTV 是 Sync + TV(一起看),SyncLive 是 Sync + Live(一起播·一起看直播)。本项目是 SyncTV 理念在直播场景的姐妹实现:一路推流,多端同步扇出。
基于 SRS 主干 + 多路分发 的同步直播服务。控制面以 protobuf / gRPC 定义,媒体面 SRS 为唯一推流入口。
- 推流仅进入 SRS 主干(WHIP),由服务端按配置扇出到多路分发。
- 分发方式:SRS HLS 直推(remux 原地出 HLS/FLV)+ Cloudflare Realtime SFU(低延时)+ 第三方直播 CDN(RTMP 转推)。
- 浏览器经服务端反向代理直连 SFU,凭证不出服务端。
- 默认 DB 使用嵌入式 libSQL (file) 持久化房间分发拓扑,零外部依赖。
- 前端为 SolidJS + TanStack Router/Query + Ark UI + Tailwind v4 单页应用,构建产物嵌入 Go 二进制。
目录结构
sync-live/
api/sync_live.proto # protobuf 控制面契约
gen/ # buf generate 产出(Go)
internal/
config/ # 运行参数 + 正式环境密钥校验
db/ # 嵌入式 libSQL 持久化
sfu/cloudflare/ # Cloudflare Realtime 分发
sfu/srs/ # SRS 主干
sfu/cdn/ # 第三方 CDN 分发
sfu/distributor.go # 分发抽象接口
server/ # gRPC 服务 + JSON 网关 + 媒体反代 + 房间拓扑 + 分发编排
static/ # 前端构建产物(由 web/ 构建生成,Go embed)
static/ # 前端构建产物(由 web/ 构建生成,Go embed)
auth/ # Casbin RBAC + JWT + 用户存储
cmd/server/main.go
deploy/ # docker-compose.yml + srs.conf
web/ # 前端工程(Solid + TanStack + Ark UI + Tailwind v4)
src/
vite.config.ts # 构建输出到 ../internal/server/static
运行
1. 配置环境变量
复制示例并填写鉴权密钥(正式环境必填):
cp .env.example .env
# 必填:JWT_SECRET(与可选 SFU_TOKEN_SECRET)
# 可选:BOOTSTRAP_ADMIN_USER / BOOTSTRAP_ADMIN_PASS 冷启动首个管理员
未配置 JWT_SECRET 时服务将拒绝启动,避免以不安全默认密钥上线。
2. 起本地 SRS(主干,必须)
cd deploy && SRS_CANDIDATE=127.0.0.1 docker compose up -d srs
SRS 为唯一推流入口,未启动时推流将失败。
3. 起控制面
go run ./cmd/server
# 首页: http://localhost:8088/
# 发布: http://localhost:8088/publish?room=<房间名>
# 观看: http://localhost:8088/watch?room=<房间名>
# 登录: http://localhost:8088/login
发布/订阅需先登录;房间名通过 URL ?room= 指定,不依赖默认占位。
鉴权(Casbin RBAC + JWT)
控制面启用登录与 RBAC:
- 发布 / 订阅 / 房间管理需登录;
guest仅可读取配置与 SRS 切片流(HLS/FLV)。 - 用户存储于
data/users.json(首次启动为空,需通过BOOTSTRAP_ADMIN_*或注册创建首个管理员)。 - 登录后服务端下发 HttpOnly Cookie,前端同源请求自动携带。
角色:admin / publisher / viewer / guest,策略见 internal/auth/policy.csv。
持久化(嵌入式 libSQL)
房间「分发目标」拓扑(stream_targets)默认持久化到嵌入式 libSQL file,而非纯内存:
- DSN:
file:./data/sync-live.db?cache=shared&_journal_mode=WAL(默认) - 兼容
DATABASE_URL覆盖;仅支持 file: / :memory:,拒绝远程 libsql:// - 自动建表,重启后自动恢复房间拓扑
curl -b cookie.txt http://localhost:8088/api/rooms | jq
运行(Cloudflare Realtime,主 SFU)
在 .env 填入 Cloudflare Realtime 凭证后启用主 SFU:
CF_APP_ID=xxxxxxxxxxxx
CF_APP_SECRET=xxxxxxxxxxxx
- 发布:服务端用
CF_APP_SECRET创建 Cloudflare session,浏览器只交换 SDP(反代注入 Bearer)。 - 观看:服务端为观众创建 viewer session,并订阅发布者 session 的轨道。
Cloudflare 未配置时 UI 标注「未配置」,SRS 链路不受影响。
protobuf / gRPC
export PATH="$HOME/go/bin:$PATH"
buf generate api
契约(SyncLive 服务):GetConfig / ListRooms / Publish / Subscribe / StopStream /
WatchRoom(服务端流式推送房间分发拓扑)以及 Login / Register / GetMe / ListUsers / UpdateUserRole。
gRPC 监听 GRPC_PORT(默认 9090),浏览器走同端口的 JSON 网关(protojson)。
前端开发(web/)
cd web
pnpm install
pnpm dev # http://localhost:5173 热更新
pnpm build # 构建进 ../internal/server/static(被 Go embed)
分流拓扑
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)
SRS 推流链路详解见
docs/streaming-pipeline.md。
主播外部推送 API(弹幕广播)
除观众可发送的 POST /api/room/{room}/chat 外,额外提供仅主播可用的独立推送端点:
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广播;同样受滑动窗口限流保护。