466 lines
26 KiB
Markdown
466 lines
26 KiB
Markdown
# SyncLive 业务逻辑文档
|
||
|
||
> 版本:2026-08-22 · 基于 `main` 分支代码整理 · 对标 SyncTV 的直播分流姐妹项目
|
||
|
||
---
|
||
|
||
## 1. 项目定位
|
||
|
||
**SyncLive = Sync + Live(一起播·一起看直播)**,对标 `SyncTV(Sync + TV 一起看)`。
|
||
|
||
- **一句话**:一路推流,多端同步扇出。控制面 `protobuf/gRPC`,媒体面 `WebRTC + HLS/FLV`。
|
||
- **双 SFU 分流**:同一路发布可同时落到 `Cloudflare Realtime(主 SFU,托管扇出)` 与 `SRS(本地对照,WHIP/WHEP/HLS/FLV 三协议同出)`。
|
||
- **设计约束**:浏览器只与服务端交换 SDP/信令,SFU 凭证不出服务端;房间拓扑持久化到嵌入式 libSQL,无外部依赖即可开箱。
|
||
|
||
```
|
||
publisher ──WHIP/tracks.new──▶ Cloudflare Realtime ──▶ viewer(s)
|
||
└────WHIP──────────▶ SRS ─┬─▶ WHEP (0.2-0.5s 低延时)
|
||
├─▶ HLS (5-10s,切片 /live/*.m3u8)
|
||
└─▶ FLV (http-flv)
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 总体架构
|
||
|
||
```
|
||
浏览器 (SolidJS SPA, TanStack Router/Query, Ark UI, Tailwind v4)
|
||
│ HTTP / SSE / WebRTC(信令经服务端反代,媒体直连 SFU)
|
||
▼
|
||
控制面 (Go, :8088 HTTP + :9090 gRPC)
|
||
├─ gRPC + JSON 网关 (protojson,见 api/sync_live.proto)
|
||
├─ 鉴权 (Casbin RBAC + JWT + UserStore)
|
||
├─ 房间拓扑 (roomHub 内存 + libSQL 持久化 stream_targets)
|
||
├─ 房间成员/权限/播放列表 (room.Service + room.Store)
|
||
├─ 聊天弹幕 (chatHub 内存广播 + 滑动窗口限流)
|
||
└─ 媒体面反代 (httputil: /rtc/v1/* → SRS, /api/cf/* → Cloudflare, /live/* → SRS HLS)
|
||
▼
|
||
媒体面 (SFU Provider 抽象)
|
||
├─ Cloudflare Realtime Provider (REST Client + BackendInfo)
|
||
└─ SRS Provider (WHIP/WHEP 信令 + HLS 静态资源)
|
||
▼
|
||
数据层
|
||
├─ libSQL 嵌入式 file:./data/sync-live.db (WAL)
|
||
└─ data/users.json (用户库,bcrypt 哈希)
|
||
```
|
||
|
||
目录映射:
|
||
|
||
| 目录 | 职责 |
|
||
|------|------|
|
||
| `api/sync_live.proto` | 控制面唯一契约,经 `buf generate` 到 `gen/` |
|
||
| `internal/config` | 环境变量加载 + `Validate()` 密钥校验 + `ProviderList()` |
|
||
| `internal/db` | 嵌入式 libSQL 打开/迁移/CRUD (stream_targets, rooms_v2 等) |
|
||
| `internal/auth` | JWT 双令牌、用户文件存储、Casbin Enforcer、中间件 |
|
||
| `internal/room` | 房间/成员/角色/权限/播放列表的领域模型与存储 |
|
||
| `internal/sfu/cloudflare` | Cloudflare Realtime REST 客户端 |
|
||
| `internal/sfu/srs` | SRS Provider 元信息 |
|
||
| `internal/server` | HTTP/gRPC 装配、网关、反代、SSE、聊天、Manage 面 |
|
||
| `cmd/server` | 进程入口与优雅关闭 |
|
||
| `web/src` | SolidJS 前端,构建产物 `internal/server/static` 被 Go embed |
|
||
| `deploy` | `docker-compose.yml` + `srs.conf` |
|
||
|
||
---
|
||
|
||
## 3. 技术栈
|
||
|
||
- **后端**:Go 1.25, `google.golang.org/grpc + protobuf`, `casbin/casbin/v2`, `golang-jwt/jwt/v5`, `tursodatabase/go-libsql`, `golang.org/x/crypto/bcrypt`
|
||
- **前端**:SolidJS + TanStack Router/Query/Virtual + Ark UI + Tailwind v4 + Vite + `hls.js` + `lucide-solid` + `playwright` (截图)
|
||
- **SFU**:Cloudflare Realtime (`rtc.live.cloudflare.com/v1`), SRS 6 (`:1935/:1985/:8080/:8000`)
|
||
- **持久化**:libSQL file 模式,`MaxOpenConns=1`,WAL
|
||
|
||
---
|
||
|
||
## 4. 核心业务模块
|
||
|
||
### 4.1 房间 (Room)
|
||
|
||
**模型** `internal/room/room.go`:
|
||
|
||
| 字段 | 含义 |
|
||
|------|------|
|
||
| `name` | 主键,URL `?room=` 指定的房间名 |
|
||
| `display_name/description` | 展示用 |
|
||
| `creator` | 创建者 username |
|
||
| `status` | `active / closed / banned`,状态机 `CanTransitionTo` |
|
||
| `is_public` | 是否公开可发现 |
|
||
| `password_hash` | 可选房间密码 (bcrypt) |
|
||
| `require_approval` | 入房需审批 → 成员 `pending` |
|
||
| `max_members` | 上限默认 100 |
|
||
| `settings_json` | `RoomSettings` JSON,含细粒度权限覆盖 |
|
||
| `auto_play` | `enabled/mode/delay`,`sequential/repeat_one/repeat_all/shuffle` |
|
||
| `category_id/cover_file_id/label_ids` | 分类/标签/封面 (兼容 SyncTV) |
|
||
|
||
**房间服务** `internal/room/service.go`:
|
||
|
||
- `CreateRoom`:建 `rooms_v2` + 写入 `creator` 为 `RoleCreator` 的成员记录
|
||
- `JoinRoom`:校验 `banned/closed`、密码、人数上限、重复加入、封禁;`require_approval=true` 时置 `MemberPending`
|
||
- `EnsurePermission/CheckPermission`:按 `RoomSettings` 与成员 `PermissionSet` 判定单项权限;`closed/banned` 房间仅允许 `view_members/view_chat_history`
|
||
- `UpdateMemberRole/Kick/Ban/Leave/ApproveJoin/RejectJoin/TransferOwnership`:均校验 `PermManageMembers/PermRemoveMembers` 与 `Role.CanManage` 等级
|
||
|
||
**成员模型** `internal/room/member.go` + `role.go`:
|
||
|
||
- 角色 `RoleCreator(1) > RoleAdmin(2) > RoleMember(3) > RoleGuest(4)`,`Rank()` 分 100/80/50/10
|
||
- 状态 `MemberActive / Banned / Pending / Kicked`
|
||
- 权限位 `PermissionSet uint64`,在 `added/removed/admin_added/admin_removed` 四列上做增量覆盖
|
||
|
||
### 4.2 鉴权与 RBAC (Auth)
|
||
|
||
**用户存储** `internal/auth/store.go` + `data/users.json`:
|
||
|
||
- 文件锁 + 内存 map,`Create` 时 bcrypt 哈希、`Verify` 比对
|
||
- `BootstrapAdmin`:用户库为空且 `BOOTSTRAP_ADMIN_USER/PASS` 已设时创建首个 `admin`(可选 `BOOTSTRAP_ADMIN_ROLE=root` 提升为 super admin)
|
||
- 字段:`username / hash / role / status(active|banned) / created_at`
|
||
|
||
**JWT** `internal/auth/jwt.go`:
|
||
|
||
- `JWTManager{secret, ttl=2h, refreshTTL=7d, issuer="sync-live"}`,HS256
|
||
- `Sign(username, role)` → `access token (TokenType=access)`;`SignRefresh` → `refresh token`
|
||
- `Verify / VerifyRefresh / IssuePair / Refresh`;`Validate()` 要求 `JWT_SECRET` 非空,否则拒绝启动
|
||
|
||
**Casbin** `internal/auth/model.conf + policy.csv + enforcer.go`:
|
||
|
||
- 模型:`r=sub,obj,act | p=sub,obj,act | g=_,_ | e=some(p.eft==allow) | m=g(r.sub,p.sub) && r.obj==p.obj && r.act==p.act`
|
||
- 策略表(节选):
|
||
- `root/admin/publisher/viewer/guest` 五档,`guest` 仅 `config:read + srs:streams + room:chat`
|
||
- `viewer` 可 `room:subscribe/watch/chat`
|
||
- `publisher` 可 `room:publish/stop`
|
||
- `admin/root` 可 `room:manage + user:list/manage + system:manage`
|
||
- `Enforcer.AddUserRole` 在登录/改角色时同步 `g` 关系
|
||
|
||
**中间件** `internal/auth/middleware.go`:
|
||
|
||
- 从 `Authorization: Bearer / Cookie(token|access_token|refresh_token) / ?token=` 提取 token
|
||
- 注入 `context.Context` 的 `AuthedUser{Username, Role, Token}`
|
||
- `AuthorizeMiddleware(obj, act, needAuth)`:`guest` 放行 `config:read + srs:streams + room:chat`,其余走 `Enforce`
|
||
|
||
### 4.3 房间分发拓扑 (roomHub + stream_targets)
|
||
|
||
**内存 Hub** `internal/server/rooms.go`:
|
||
|
||
- `roomHub{rooms map[name]*roomEntry, db *sql.DB}`,`roomEntry{targets map[backend]*StreamTarget, subs map[chan]struct{}}`
|
||
- `setTarget(room, backend, target)`:写入内存 → `broadcast(room)` → 异步 `db.SaveTarget`
|
||
- `removeTarget`:删 backend,空房间且无订阅者则删 entry + `db.DeleteTarget`
|
||
- `subscribe(room)`:每房间 8 缓冲 channel,`broadcast` 非阻塞 fan-out
|
||
- `broadcastEvent(room, type, payload)`:复用同一订阅通道,以 `{"type": "...", "payload": ...}` 信封推送 `playlist_update / playback` 等通用事件
|
||
- `newRoomHubWithDB` 启动时 `db.LoadAll` 恢复全量房间
|
||
|
||
**持久化** `internal/db/db.go`:
|
||
|
||
- `Open(dsn)` 仅允许 `file:` / `:memory:`,自动 `MkdirAll` + `Migrate`
|
||
- 表:
|
||
- `stream_targets(room, backend PK, session_id, stream, publish_token, url, published_at)` + `idx_room`
|
||
- `rooms(name PK, created_at, updated_at)` (轻量索引)
|
||
- `rooms_v2 / room_members / room_bans / room_join_requests / room_categories / room_labels / user_bans / user_registration_requests`
|
||
- `SaveTarget = INSERT OR REPLACE` + 维护 `rooms` 占位;`DeleteTarget` 空房间时删 `rooms`
|
||
|
||
**控制面 API** `internal/server/service.go + gateway.go`:
|
||
|
||
- `GetConfig`:按 `SFU_PROVIDER` 顺序组 `BackendInfo[] + candidate + token_required`
|
||
- `Publish(room, backend, identity)`:
|
||
- `cloudflare`:`CreateSession(room) → GetSession → target{session_id}`;未配置时直接报错
|
||
- `srs`:`stream=live-<room>` + `signStreamToken(secret, stream, identity, "publish", 2h)` → `target{stream, publish_token, url=/rtc/v1/whep/?app=live&stream=...}`
|
||
- 统一 `hub.setTarget` 并广播
|
||
- `Subscribe(room, backend)`:
|
||
- `cloudflare`:新建 viewer session,返回 `session_id + publisher_session_id + ice_servers`
|
||
- `srs`:返回 `stream + ice_servers(stun)`,观看侧 WHEP 与 HLS 共用同一 `stream`
|
||
- `StopStream(room, backend)`:`cloudflare` 删发布 session + `hub.removeTarget`
|
||
- `WatchRoom (gRPC stream) / GET /api/room/{room}/events (SSE)`:首包即时推送 `targets`,后续 fan-out + 20s 心跳全量重推
|
||
- `handleRoomsQuery GET /api/rooms/query`:支持 `page/page_size/search/status/creator/is_public/sort_by/sort_direction`,优先走 `room.Store.QueryRooms` (DB),否则回退内存 hub
|
||
|
||
### 4.4 媒体面反代与 SFU
|
||
|
||
**SRS** `internal/server/proxy.go + deploy/srs.conf`:
|
||
|
||
- `srsProxyHandler`:`SRS_API_BASE (默认 :1985)` 的 WHIP/WHEP 信令反代;`SFU_TOKEN_REQUIRED=1` 时校验 `?token=` 为 `role=publish` 且 `room` 一致的 HS256 JWT
|
||
- `srsHlsProxyHandler`:`SRS_HTTP_BASE (默认 :8080)` 的 HLS/FLV 静态切片反代,挂 `GET/HEAD /live/`,自动补 CORS `Allow-Origin: *`,`OPTIONS` 204
|
||
- SRS 配置:`http_server :8080` 切片落盘 `objs/nginx/html`,`http_api :1985` 信令,`rtc_server :8000` 媒体,`vhost __defaultVhost__ { rtc + http_remux(flv) + hls(fragment 10s, window 60s) }`
|
||
|
||
**Cloudflare** `internal/sfu/cloudflare/client.go + provider.go`:
|
||
|
||
- `Provider{client, appID, stun, configured}`,`BackendInfo{kind=CLOUDFLARE, name="Cloudflare Realtime", configured, primary=true}`
|
||
- `cfProxyHandler`:将 `/api/cf/*` 转 `CF_BASE_URL/apps/<CF_APP_ID>/*`,注入 `Authorization: Bearer CF_APP_SECRET`,浏览器只做 `tracks/new` SDP 交换
|
||
|
||
**前端媒体链路**:
|
||
|
||
- 推流:`RTCPeerConnection → createOffer → POST /rtc/v1/whip/?app=live&stream=live-<room>&token=... (信令经网关)` → `setRemoteDescription(answer)`,成功后展示 `hlsUrl=/live/<stream>.m3u8 + flvUrl`
|
||
- 观看:
|
||
- `WHEP`:`POST /rtc/v1/whep/?app=live&stream=...` 同流程,`ontrack → <video>`
|
||
- `HLS`:`video.canPlayType('application/vnd.apple.mpegurl')` 走原生,否则 `hls.js loadSource/attachMedia`;观看页三档切 `cloudflare / srs(whep) / srs-hls`
|
||
|
||
### 4.5 聊天弹幕
|
||
|
||
**hub** `internal/server/chat.go`:
|
||
|
||
- `chatMessage{id, room, user, role, message, color, host, ts}`;`chatHub{rooms map[room]map[chan]struct{}, recentMsgs map[room][]*chatMessage}`,`recentLimit=50`
|
||
- `subscribe(room)` 16 缓冲;`broadcast` 进 `recentMsgs` 并裁剪;`recent(room)` 快照回放
|
||
- 空房间无订阅者时清 `recentMsgs`,防内存无界
|
||
- `GET /api/room/{room}/chat (SSE)`:进场回放 recent + 25s ping;`POST /api/room/{room}/chat` 与 `POST /api/room/{room}/broadcast` (主播专用,`host=true`) 均经 `chatHub.broadcast`
|
||
- 限流 `chatRateLimiter(limit=5, window=3s)`,key=`room/user/ip` (broadcast 另缀 `/broadcast/`),滑动窗口 + 分钟级 `cleanup`
|
||
- `clientIP` 取 `X-Forwarded-For[0] / X-Real-IP / RemoteAddr`
|
||
|
||
**权限**:`POST /chat` 需 `room:chat` (guest 亦可);`POST /broadcast` 需 `room:publish` (publisher/admin),走 `authWrap(..., needAuth=true)`
|
||
|
||
### 4.6 播放列表与同步播放
|
||
|
||
`internal/server/playlist.go + internal/room/playlist.go`:
|
||
|
||
- `PlaylistItem{room, url, title, added_by}`,`PlaybackState{room, position, playing, rate, current_item, updated_by}`
|
||
- `GET /api/room/{room}/playlist` 列件;`POST` (需 `PermControlStream`) 增件并 `broadcastEvent("playlist_update", items)`;`DELETE ?id=` 删件同广播
|
||
- `GET /api/room/{room}/playback` 读状态(无则 `rate=1`);`POST/PUT` 需 `PermControlStream`,`rate<=0` 归一,落库后 `broadcastEvent("playback", ps)`,观看端经同一 SSE 连接按 `event: playlist_update / playback` 分发
|
||
|
||
### 4.7 房间内权限位
|
||
|
||
`internal/room/permission.go`:
|
||
|
||
- 16 项:`send_chat / view_chat_history / view_members / subscribe / watch_room / publish / stop_stream / broadcast / manage_members / add_members / remove_members / manage_permissions / manage_room / delete_room / delete_chat_messages / control_stream`
|
||
- `PermissionSet uint64` 位集,`Grant/Revoke/Has/Names`
|
||
- 默认集:
|
||
- `PermDefaultCreator = All`
|
||
- `PermDefaultAdmin` = 除 `delete_room` 外大部分
|
||
- `PermDefaultMember` = `chat/view/subscribe/watch/publish/stop`
|
||
- `PermDefaultGuest` = `view_chat/view_members/subscribe/watch`;`PermGuestAssignable = Guest + send_chat`
|
||
- `RoomSettings.Admin/Member/GuestPermissions()` 在 `Default*` 上叠加 `added/removed` 增量
|
||
|
||
### 4.8 管理后台 (Manage)
|
||
|
||
`internal/server/manage.go` 挂于 `/api/manage/*`,均 `authWrap(needAuth=true)`:
|
||
|
||
- `GET /api/manage/overview`:汇总计数
|
||
- `GET/POST /api/manage/rooms`:列/建房间(含 `is_public/require_approval/max_members/password/display_name/description`)
|
||
- `GET/PUT/PATCH/DELETE /api/manage/rooms/{room}`:详情/改设置/删房
|
||
- `GET/POST /api/manage/rooms/{room}/members`、`PUT/DELETE .../members/{user}`、`POST .../perms`、`POST .../approve|reject`、`POST .../transfer`:成员增删改、权限覆盖、审批、转让房主 (`RoleCreator` 互转)
|
||
- `GET /api/manage/permissions[/{room}]`:权限矩阵
|
||
- `GET/PUT /api/manage/rooms/{room}/settings`、`POST .../join|leave`:房间设置与自助进出
|
||
|
||
---
|
||
|
||
## 5. 关键流程时序
|
||
|
||
### 5.1 登录 / 注册 / 鉴权
|
||
|
||
1. `POST /api/auth/login|register {username,password,role?}` → `UserStore.Verify/Create` → `JWT.Sign` → `Set-Cookie: token=... HttpOnly + {token,user,expires_at}`
|
||
2. 后续请求带 `Cookie` 或 `Authorization: Bearer`,`AuthorizeMiddleware` → `JWT.Verify` → 注入 `AuthedUser` → `Enforcer.Enforce(sub,obj,act)`
|
||
3. `POST /api/auth/refresh` 用 `refresh_token` 换新 `access`;`GET /api/auth/me` 返回当前用户与 `expires_at/issued_at`
|
||
4. 未登录 `guest` 默认仅 `config:read` 与 `srs:streams` 与 `room:chat` 放行,其余 401/403
|
||
|
||
### 5.2 房间生命周期
|
||
|
||
1. `POST /api/rooms {name}` 或 `POST /api/manage/rooms` → `hub.createRoom` (内存占位);若走 `room.Service.CreateRoom` 则同时落 `rooms_v2 + room_members(creator)`
|
||
2. `GET /api/rooms | GET /api/rooms/query?...` 分页查;`GET /api/manage/rooms/{room}` 看成员与 `targets(live)`;`GET /live/*.m3u8` 经网关拉 HLS 无需订阅
|
||
3. 转让:`POST /api/manage/rooms/{room}/transfer {target}` 需当前 `RoleCreator`,事务内改 `rooms_v2.creator` 并互换 `creator↔admin` 角色
|
||
4. 关闭/封禁:`RoomStatus` 流转 `active↔closed/banned`,`EnsurePermission` 据此限权
|
||
|
||
### 5.3 发布 (Publish)
|
||
|
||
```
|
||
前端 → POST /api/publish {room, backend} (需 room:publish)
|
||
→ Service.Publish 按 backend 分流
|
||
├─ cloudflare: CreateSession(room) → target{session_id}
|
||
└─ srs: stream=live-<room>, token=HS256(room,identity,publish,2h) → target{stream,token,url}
|
||
→ hub.setTarget(room, backend, target) → SSE 广播 RoomEvent{targets}
|
||
→ 返回 {session_id/stream/publish_token/ice_servers/target}
|
||
前端 → RTCPeerConnection offer → POST /rtc/v1/whip/?app=live&stream=...&token=... (SRS 反代校验) 或 POST /api/cf/.../tracks/new (CF 反代注入 Bearer)
|
||
→ setRemoteDescription(answer) → 推流成功,展示 HLS/FLV 链接
|
||
```
|
||
|
||
### 5.4 订阅 / 观看
|
||
|
||
```
|
||
前端 → POST /api/subscribe {room, backend} (需 room:subscribe)
|
||
→ hub.findTarget(room, backend) 取发布侧 target
|
||
├─ cloudflare: CreateSession(room) viewer → {session_id, publisher_session_id, ice_servers}
|
||
└─ srs: {stream, ice_servers}
|
||
前端 → 按 backends[].configured 展示线路,未配置标「未配置」
|
||
├─ cloudflare: tracks/new location=remote
|
||
├─ srs-whep: POST /rtc/v1/whep/?app=live&stream=live-<room> → ontrack
|
||
└─ srs-hls: GET /live/live-<room>.m3u8 → hls.js / 原生
|
||
→ 同时订阅 GET /api/room/{room}/events (SSE) + GET /api/room/{room}/chat (SSE),拓扑/弹幕/播放状态同链路按 event type 分发
|
||
```
|
||
|
||
### 5.5 弹幕与主播广播
|
||
|
||
```
|
||
观众 POST /api/room/{room}/chat {message,color} (room:chat)
|
||
→ 取身份 user/role/id (guest 可发)
|
||
→ chatRL.allow(room/user/ip, 5/3s) 限流
|
||
→ chatHub.broadcast → SSE fan-out (event: chat) → 前端 danmaku 叠加
|
||
|
||
主播 POST /api/room/{room}/broadcast {message,color} (room:publish, 需 Bearer)
|
||
→ 同限流 key=room/broadcast/user/ip
|
||
→ chatMessage{host:true} → 同一 hub 广播,观众/主播端同可见
|
||
```
|
||
|
||
### 5.6 停止
|
||
|
||
`POST /api/stop {room, backend} (room:stop)` → `cloudflare` 删 `session_id` + `hub.removeTarget(room, backend)` → 广播空 targets → 前端自动切离或提示下播
|
||
|
||
---
|
||
|
||
## 6. 数据模型与契约
|
||
|
||
### 6.1 Protobuf 契约 `api/sync_live.proto`
|
||
|
||
- 服务 `SyncLive`:`GetConfig / ListRooms / Publish / Subscribe / StopStream / WatchRoom(stream) / Login / Register / GetMe / ListUsers / UpdateUserRole`
|
||
- 枚举 `BackendKind`: `UNSPECIFIED(0) / CLOUDFLARE(1) / SRS(2)`
|
||
- 消息:`BackendInfo{kind,name,configured,primary}`, `IceServer{urls,username,credential}`, `StreamTarget{backend,session_id,stream,publish_token,url,published_at}`, `Room{name,targets[]}`, `RoomEvent{room,targets[]}`, `UserInfo{username,role,created_at}`, `Login/Register/GetMe/ListUsers/UpdateUserRole` 族
|
||
|
||
### 6.2 libSQL 表
|
||
|
||
- `stream_targets`:房间分发的事实表,`PRIMARY KEY(room,backend)`
|
||
- `rooms / rooms_v2`:`rooms` 为轻量拓扑索引,`rooms_v2` 为完整房间档案(含 `settings_json`)
|
||
- `room_members(room,user_id PK, username, role, status, added/removed/admin_added/admin_removed, joined_at, version)` + 两索引
|
||
- `room_bans / room_join_requests / room_categories / room_labels / user_bans / user_registration_requests`
|
||
- 增量兼容:启动时 `ALTER TABLE rooms_v2 ADD COLUMN category_id/cover_file_id/label_ids` 忽略已存在错误
|
||
|
||
### 6.3 用户库 `data/users.json`
|
||
|
||
- `map[username]*User{username, hash(bcrypt), role, status, created_at}`,文件锁读写,`BOOTSTRAP_ADMIN_*` 冷启动种子
|
||
|
||
---
|
||
|
||
## 7. API 清单
|
||
|
||
### 7.1 JSON 网关 (同 HTTP 端口,protojson)
|
||
|
||
| 方法 | 路径 | 鉴权 | 说明 |
|
||
|------|------|------|------|
|
||
| GET | `/api/config` | `config:read` guest可 | 后端能力与 `candidate/token_required` |
|
||
| GET | `/api/rooms` | `room:list` | 全量房间 (hub) |
|
||
| GET | `/api/rooms/query?...` | `room:list` | 分页查询 (DB 优先) |
|
||
| GET | `/api/rooms/events` | `room:list` SSE | 全局房间变更(create/update/delete/sync,`event: rooms`),供 /rooms 实时刷新 |
|
||
| POST | `/api/rooms` | `room:list` | 创建占位 |
|
||
| POST | `/api/publish` | `room:publish` | 发布 (多后端) |
|
||
| POST | `/api/subscribe` | `room:subscribe` | 订阅 |
|
||
| POST | `/api/stop` | `room:stop` | 下播 |
|
||
| GET | `/api/srs/streams` | `srs:streams` guest可 | 代理 `SRS :1985 /api/v1/streams/` |
|
||
| GET | `/api/room/{room}/events` | `room:watch` SSE | 房间拓扑 |
|
||
| GET | `/api/room/{room}/playlist` | `room:watch` | 列播放列表 |
|
||
| POST | `/api/room/{room}/playlist` | `room:watch` + `control_stream` | 增件 |
|
||
| DELETE | `/api/room/{room}/playlist?id=` | 同上 | 删件 |
|
||
| GET | `/api/room/{room}/playback` | `room:watch` | 读同步状态 |
|
||
| POST/PUT | `/api/room/{room}/playback` | 同上 | 写同步状态 |
|
||
| GET | `/api/room/{room}/chat` | `room:chat` SSE guest可 | 弹幕订阅 + recent 回放 |
|
||
| POST | `/api/room/{room}/chat` | `room:chat` guest可 | 发弹幕 |
|
||
| POST | `/api/room/{room}/broadcast` | `room:publish` | 主播广播 (`host:true`) |
|
||
|
||
### 7.2 认证
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| POST | `/api/auth/login` | 登录,签 access+refresh,种 HttpOnly Cookie |
|
||
| POST | `/api/auth/register` | 注册,`ALLOW_REGISTER=1` 时放行,`admin/publisher` 需 admin 授权否则降为 viewer |
|
||
| POST | `/api/auth/logout` | 清 Cookie |
|
||
| POST | `/api/auth/refresh` | 用 refresh 换 access |
|
||
| GET | `/api/auth/me` | 当前用户 |
|
||
| GET | `/api/auth/users` | 列用户 (admin) |
|
||
| POST | `/api/auth/users/role` | 改角色 (`only root can grant root`, admin 不能提权他人为 admin/root) |
|
||
| POST | `/api/auth/users/ban|unban` | 封/解封 (admin/root) |
|
||
| GET | `/api/auth/check` | 前端权限探针 |
|
||
|
||
### 7.3 管理面 `/api/manage/*`
|
||
|
||
见 4.8 节,覆盖 `overview/rooms/rooms/{room}/members/permissions/settings/join/leave/transfer` 全链路。
|
||
|
||
### 7.4 媒体反代
|
||
|
||
| 方法 | 路径 | 目标 | 说明 |
|
||
|------|------|------|------|
|
||
| ANY | `/rtc/v1/*` | `SRS_API_BASE (:1985)` | WHIP/WHEP 信令,可选 `?token` 校验 |
|
||
| ANY | `/api/cf/*` | `CF_BASE_URL/apps/<CF_APP_ID>/*` | Cloudflare Realtime,注 Bearer |
|
||
| GET/HEAD | `/live/*` | `SRS_HTTP_BASE (:8080)` | HLS/FLV 切片,补 CORS |
|
||
|
||
### 7.5 gRPC `:9090`
|
||
|
||
同 `SyncLive` 服务,`UnaryAuthInterceptor + StreamAuthInterceptor` 校验 `room/user/config/srs/system` 等 `obj:act`。
|
||
|
||
---
|
||
|
||
## 8. 权限矩阵
|
||
|
||
### 8.1 全局 Casbin (policy.csv)
|
||
|
||
| 角色 | 能力 |
|
||
|------|------|
|
||
| `root` | 全量:`config:read, room:list/publish/subscribe/stop/watch/chat/manage/delete, user:list/manage, srs:streams, system:manage` |
|
||
| `admin` | 除 `system:manage/room:delete` 外全量 |
|
||
| `publisher` | `config:read + room:list/publish/subscribe/stop/watch/chat + srs:streams` |
|
||
| `viewer` | `config:read + room:list/subscribe/watch/chat + srs:streams` |
|
||
| `guest` | `config:read + srs:streams + room:chat` (弹幕可发,拉流只看 HLS) |
|
||
|
||
### 8.2 房间内位权限 (PermissionSet)
|
||
|
||
见 4.7 表,房间设置可在默认集上按 `admin/member/guest` 三档增删位,且受 `Role.CanManage` 等级约束。关键校验入口 `room.Service.EnsurePermission`。
|
||
|
||
---
|
||
|
||
## 9. 配置与部署
|
||
|
||
### 9.1 环境变量 (env > 默认)
|
||
|
||
| 变量 | 默认 | 说明 |
|
||
|------|------|------|
|
||
| `HTTP_PORT/GRPC_PORT` | `8088/9090` | 控制面端口 |
|
||
| `JWT_SECRET` | (必填) | 正式环境必须显式配置,否则 `Validate` 拒绝启动 |
|
||
| `SFU_TOKEN_SECRET` | 同 JWT | 推流 JWT 密钥,空则复用 JWT |
|
||
| `SFU_TOKEN_REQUIRED` | `0` | `1` 时 WHIP 强制 `?token` 校验 |
|
||
| `SFU_PROVIDER` | `cloudflare,srs` | 后端顺序 |
|
||
| `SRS_API_BASE/SRS_HTTP_BASE/SRS_APP/SRS_CANDIDATE` | `http://localhost:1985 / :8080 / live / 127.0.0.1` | SRS 链路 |
|
||
| `CF_APP_ID/CF_APP_SECRET/CF_BASE_URL/CF_STUN_URL` | (空) / `https://rtc.live.cloudflare.com/v1` / `stun:stun.cloudflare.com:3478` | Cloudflare 链路 |
|
||
| `TURSO_DATABASE_URL/DATABASE_URL` | `file:./data/sync-live.db?cache=shared&_journal_mode=WAL` | 仅 file/`:memory:`,拒 `libsql://` |
|
||
| `AUTH_USER_FILE/CASBIN_MODEL/CASBIN_POLICY` | `data/users.json / internal/auth/model.conf / policy.csv` | 认证文件 |
|
||
| `ALLOW_REGISTER` | `1` | 是否开放注册 |
|
||
| `BOOTSTRAP_ADMIN_USER/PASS/ROLE` | (空) | 冷启动种子,仅空库生效 |
|
||
| `SMTP_*/EMAIL_VERIFY_*/OAUTH_*/AUTHBOSS_ENABLED` | (多为 `0`/空) | 邮件/验证/OAuth 预留开关 |
|
||
|
||
### 9.2 本地启动
|
||
|
||
```bash
|
||
cp .env.example .env # 填 JWT_SECRET 等
|
||
cd deploy && SRS_CANDIDATE=127.0.0.1 docker compose up -d srs
|
||
go run ./cmd/server # http://localhost:8088 /publish?room=xxx /watch?room=xxx /login /manage
|
||
# 前端热更新
|
||
cd web && pnpm i && pnpm dev # :5173
|
||
pnpm build # 产到 internal/server/static (Go embed)
|
||
```
|
||
|
||
### 9.3 SRS 部署要点
|
||
|
||
- `http_server :8080` 切片目录与 `hls_path` 一致;`hls_fragment 10 / window 60`;公网部署 `SRS_CANDIDATE=公网IP` 并重建容器
|
||
- 生产可不暴露 `8080`,仅保留 `1985/8000`,前端统一经 `8088 /live/*` 拉流
|
||
- HLS 首屏 10-20s,重试 `curl /live/<stream>.m3u8` 与 `docker exec ls objs/nginx/html/live` 排障
|
||
|
||
---
|
||
|
||
## 10. 前端
|
||
|
||
- 路由 `web/src/app/router.tsx`:`TanStack Router + RootRoute(Layout) + lazy(code-split)`,路由 `/ /publish?room= /watch?room= /room?room= /rooms /users /manage /login`,均为 SPA 回退 `index.html`
|
||
- 状态:`TanStack Query` 封装 `api.ts` 的 `fetch /api/* (protojson)`;SSE 封装 `sse.ts/chat.ts`,`GET /api/room/{room}/events` 与 `/chat` 并行订阅,按 `event: room/chat/playlist_update/playback` 分发
|
||
- 媒体:`webrtc.ts` 封装 WHIP/WHEP 的 `RTCPeerConnection` 流程,`hls.ts` 封装 `hls.js` 回退
|
||
- 组件:`components/danmaku.tsx` 叠加层,`components/manage/*` 管理后台分 `Overview/Rooms/Permissions/System/UsersInline`
|
||
- 构建:`vite.config.ts` 输出 `../internal/server/static`,`internal/server/server.go:embed static` 内联,`GET / /publish /watch /login /manage` 均回 `static/index.html`
|
||
|
||
---
|
||
|
||
## 11. 安全与限流
|
||
|
||
- 正式环境密钥强校验:缺 `JWT_SECRET` 或 `SFU_TOKEN_REQUIRED` 却无 `SFU_TOKEN_SECRET` 直接退错
|
||
- 密码:用户 `bcrypt`,房间密码 `bcrypt` (`HashRoomPassword`)
|
||
- 推流 token:`token.go` 自实现 `HS256(b64url header + payload).hmacSHA256(secret)`,`{room,identity,role,iat,exp}`,TTL 2h
|
||
- 弹幕限流:每 `room/user/ip` 5 条 / 3s 滑动窗口,超限 `429`,后台分钟级清过期 key;单条 500 字符截断
|
||
- 鉴权覆盖:HTTP `authWrap` 与 gRPC `Unary/StreamInterceptor` 双轨,`guest` 白名单最小化
|
||
|
||
---
|
||
|
||
## 12. 关联文档与验证
|
||
|
||
- 推流链路详述:`docs/streaming-pipeline.md`(含 SRS `srs.conf` 全量、反代代码、前端 `publishSRS/watchSRS/watchHLS` 选型表与 `curl/ffplay` 验证步骤)
|
||
- 架构分层:`design.md`
|
||
- 协议契约:`api/sync_live.proto` + `buf.gen.yaml`
|
||
- 快速验证:发布页推流后 `curl -i http://localhost:8088/live/live-demo.m3u8` / `curl -I .../live-demo.flv` / `ffplay ...`,SRS 日志与切片目录联合排障
|
||
|
||
---
|
||
|
||
*维护提示:新增后端或房间字段时,同步更新 `api/*.proto → gen/`、`internal/db.Migrate / room.Store.InitSchema`、`policy.csv` 与本文件对应小节;房间拓扑的持久化与 SSE 广播是分流可见性的核心链路,改动前优先补 `grpc_test.go / auth_test.go` 用例。*
|
||
|