live-sfu-demo/docs/business-logic.md

26 KiB
Raw Permalink Blame History

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`
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/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 用例。