package server import ( "context" "database/sql" "encoding/json" "log" "sync" "time" "sync-live/gen" "sync-live/internal/db" ) // roomHub 维护房间 -> 各后端分发目标(StreamTarget)的内存状态, // 并向订阅者广播拓扑变化(实现「分流」状态实时可见)。 // 默认由嵌入式 libSQL (file) 持久化。 type roomHub struct { mu sync.RWMutex rooms map[string]*roomEntry db *sql.DB globalSubs map[chan []byte]struct{} } type roomEntry struct { targets map[string]*gen.StreamTarget // key: 后端名(cloudflare / srs) distributions map[string]*gen.DistributionTarget // key: distribution kind (srs-hls/cf/cdn) subs map[chan []byte]struct{} } func newRoomHub() *roomHub { return &roomHub{rooms: map[string]*roomEntry{}, globalSubs: map[chan []byte]struct{}{}} } // newRoomHubWithDB 创建带嵌入式 libSQL 持久化的 hub,启动时自动从 DB 加载全量房间。 func newRoomHubWithDB(database *sql.DB) *roomHub { h := &roomHub{rooms: map[string]*roomEntry{}, db: database, globalSubs: map[chan []byte]struct{}{}} if database != nil { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() all, err := db.LoadAll(ctx, database) if err != nil { log.Printf("[hub] load from embedded db failed: %v", err) } else { for room, backends := range all { e := &roomEntry{targets: map[string]*gen.StreamTarget{}, distributions: map[string]*gen.DistributionTarget{}, subs: map[chan []byte]struct{}{}} for backend, t := range backends { e.targets[backend] = t } h.rooms[room] = e } if len(all) > 0 { log.Printf("[hub] loaded %d rooms from embedded db", len(all)) } } } return h } // DB 返回底层 *sql.DB,供优雅关闭。 func (h *roomHub) DB() *sql.DB { return h.db } func (h *roomHub) get(name string) *roomEntry { e, ok := h.rooms[name] if !ok { e = &roomEntry{targets: map[string]*gen.StreamTarget{}, distributions: map[string]*gen.DistributionTarget{}, subs: map[chan []byte]struct{}{}} h.rooms[name] = e } return e } // createRoom 确保房间占位存在(发布前即出现在列表)。 func (h *roomHub) createRoom(name string) { h.mu.Lock() if _, ok := h.rooms[name]; !ok { h.rooms[name] = &roomEntry{targets: map[string]*gen.StreamTarget{}, distributions: map[string]*gen.DistributionTarget{}, subs: map[chan []byte]struct{}{}} h.mu.Unlock() h.broadcastRooms("create", name) return } h.mu.Unlock() } func (h *roomHub) setTarget(room, backend string, t *gen.StreamTarget) { h.mu.Lock() e := h.get(room) e.targets[backend] = t h.broadcast(room, e) h.broadcastRoomsLocked("update", room) h.mu.Unlock() if h.db != nil { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := db.SaveTarget(ctx, h.db, room, backend, t); err != nil { log.Printf("[hub] save target %s/%s to embedded db: %v", room, backend, err) } } } func (h *roomHub) removeTarget(room, backend string) { h.mu.Lock() e, ok := h.rooms[room] if !ok { h.mu.Unlock() return } delete(e.targets, backend) if len(e.targets) == 0 && len(e.subs) == 0 { delete(h.rooms, room) h.broadcastRoomsLocked("delete", room) h.mu.Unlock() if h.db != nil { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := db.DeleteTarget(ctx, h.db, room, backend); err != nil { log.Printf("[hub] delete target %s/%s: %v", room, backend, err) } } return } h.broadcast(room, e) h.broadcastRoomsLocked("update", room) h.mu.Unlock() if h.db != nil { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := db.DeleteTarget(ctx, h.db, room, backend); err != nil { log.Printf("[hub] delete target %s/%s: %v", room, backend, err) } } } func (h *roomHub) targets(room string) []*gen.StreamTarget { h.mu.RLock() defer h.mu.RUnlock() e, ok := h.rooms[room] if !ok { return nil } out := make([]*gen.StreamTarget, 0, len(e.targets)) for _, t := range e.targets { out = append(out, t) } return out } func (h *roomHub) list() []*gen.Room { h.mu.RLock() defer h.mu.RUnlock() out := make([]*gen.Room, 0, len(h.rooms)) for name, e := range h.rooms { room := &gen.Room{Name: name} for _, t := range e.targets { room.Targets = append(room.Targets, t) } for _, d := range e.distributions { room.Distributions = append(room.Distributions, d) } out = append(out, room) } return out } func (h *roomHub) subscribe(room string) (chan []byte, func()) { h.mu.Lock() defer h.mu.Unlock() e := h.get(room) ch := make(chan []byte, 8) e.subs[ch] = struct{}{} return ch, func() { h.mu.Lock() defer h.mu.Unlock() if e2, ok := h.rooms[room]; ok { delete(e2.subs, ch) } } } func (h *roomHub) broadcast(room string, e *roomEntry) { payload, _ := json.Marshal(map[string]interface{}{ "room": room, "targets": e.targets, "distributions": e.distributions, }) for ch := range e.subs { select { case ch <- payload: default: } } } // broadcastEvent 向房间订阅者广播一个带类型的通用事件(playlist / playback / presence 等)。 // 复用与拓扑相同的订阅通道,前端在同一个 SSE 连接上按 event type 分发处理。 func (h *roomHub) broadcastEvent(room, eventType string, payload interface{}) { h.mu.RLock() e, ok := h.rooms[room] if !ok { h.mu.RUnlock() return } envelope, _ := json.Marshal(map[string]interface{}{ "type": eventType, "room": room, "payload": payload, }) for ch := range e.subs { select { case ch <- envelope: default: } } h.mu.RUnlock() } // ---- 全局房间列表广播(供 /api/rooms/events 订阅) ---- func (h *roomHub) subscribeRooms() (chan []byte, func()) { h.mu.Lock() defer h.mu.Unlock() if h.globalSubs == nil { h.globalSubs = map[chan []byte]struct{}{} } ch := make(chan []byte, 8) h.globalSubs[ch] = struct{}{} return ch, func() { h.mu.Lock() defer h.mu.Unlock() delete(h.globalSubs, ch) } } func (h *roomHub) broadcastRooms(event, room string) { h.mu.RLock() defer h.mu.RUnlock() h.broadcastRoomsLocked(event, room) } func (h *roomHub) broadcastRoomsLocked(event, room string) { if len(h.globalSubs) == 0 { return } payload, _ := json.Marshal(map[string]interface{}{ "type": "rooms", "event": event, "room": room, "ts": time.Now().Unix(), }) for ch := range h.globalSubs { select { case ch <- payload: default: } } } func (h *roomHub) distributions(room string) []*gen.DistributionTarget { h.mu.RLock() defer h.mu.RUnlock() e, ok := h.rooms[room] if !ok { return nil } out := make([]*gen.DistributionTarget, 0, len(e.distributions)) for _, d := range e.distributions { out = append(out, d) } return out } func (h *roomHub) setDistribution(room string, d *gen.DistributionTarget) { h.mu.Lock() e := h.get(room) if e.distributions == nil { e.distributions = map[string]*gen.DistributionTarget{} } key := d.GetKind().String() e.distributions[key] = d h.broadcast(room, e) h.broadcastRoomsLocked("update", room) h.mu.Unlock() } func (h *roomHub) clearDistributions(room string) { h.mu.Lock() if e, ok := h.rooms[room]; ok { e.distributions = map[string]*gen.DistributionTarget{} h.broadcast(room, e) h.broadcastRoomsLocked("update", room) } h.mu.Unlock() } func (h *roomHub) removeRoom(name string) { h.mu.Lock() defer h.mu.Unlock() delete(h.rooms, name) payload, _ := json.Marshal(map[string]interface{}{"type": "rooms", "event": "delete", "room": name, "ts": time.Now().Unix()}) for ch := range h.globalSubs { select { case ch <- payload: default: } } }