25 KiB
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时置MemberPendingEnsurePermission/CheckPermission:按RoomSettings与成员PermissionSet判定单项权限;closed/banned房间仅允许view_members/view_chat_historyUpdateMemberRole/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"},HS256Sign(username, role)→access token (TokenType=access);SignRefresh→refresh tokenVerify / 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:chatviewer可room:subscribe/watch/chatpublisher可room:publish/stopadmin/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.SaveTargetremoveTarget:删 backend,空房间且无订阅者则删 entry +db.DeleteTargetsubscribe(room):每房间 8 缓冲 channel,broadcast非阻塞 fan-outbroadcastEvent(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_roomrooms(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_requiredPublish(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_serverssrs:返回stream + ice_servers(stun),观看侧 WHEP 与 HLS 共用同一stream
StopStream(room, backend):cloudflare删发布 session +hub.removeTargetWatchRoom (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 JWTsrsHlsProxyHandler:SRS_HTTP_BASE (默认 :8080)的 HLS/FLV 静态切片反代,挂GET/HEAD /live/,自动补 CORSAllow-Origin: *,OPTIONS204- 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/newSDP 交换
前端媒体链路:
- 推流:
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=50subscribe(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 = AllPermDefaultAdmin= 除delete_room外大部分PermDefaultMember=chat/view/subscribe/watch/publish/stopPermDefaultGuest=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 登录 / 注册 / 鉴权
POST /api/auth/login|register {username,password,role?}→UserStore.Verify/Create→JWT.Sign→Set-Cookie: token=... HttpOnly + {token,user,expires_at}- 后续请求带
Cookie或Authorization: Bearer,AuthorizeMiddleware→JWT.Verify→ 注入AuthedUser→Enforcer.Enforce(sub,obj,act) POST /api/auth/refresh用refresh_token换新access;GET /api/auth/me返回当前用户与expires_at/issued_at- 未登录
guest默认仅config:read与srs:streams与room:chat放行,其余 401/403
5.2 房间生命周期
POST /api/rooms {name}或POST /api/manage/rooms→hub.createRoom(内存占位);若走room.Service.CreateRoom则同时落rooms_v2 + room_members(creator)GET /api/rooms | GET /api/rooms/query?...分页查;GET /api/manage/rooms/{room}看成员与targets(live);GET /live/*.m3u8经网关拉 HLS 无需订阅- 转让:
POST /api/manage/rooms/{room}/transfer {target}需当前RoleCreator,事务内改rooms_v2.creator并互换creator↔admin角色 - 关闭/封禁:
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 优先) |
| 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` |
| 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 本地启动
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/ip5 条 / 3s 滑动窗口,超限429,后台分钟级清过期 key;单条 500 字符截断 - 鉴权覆盖:HTTP
authWrap与 gRPCUnary/StreamInterceptor双轨,guest白名单最小化
12. 关联文档与验证
- 推流链路详述:
docs/streaming-pipeline.md(含 SRSsrs.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 用例。