package server import ( "encoding/json" "sync" "gospeak-live-sfu-demo/gen" ) // roomHub 维护房间 -> 各后端分发目标(StreamTarget)的内存状态, // 并向订阅者广播拓扑变化(实现「分流」状态实时可见)。 type roomHub struct { mu sync.RWMutex rooms map[string]*roomEntry } type roomEntry struct { targets map[string]*gen.StreamTarget // key: 后端名(cloudflare / srs) subs map[chan []byte]struct{} } func newRoomHub() *roomHub { return &roomHub{rooms: map[string]*roomEntry{}} } func (h *roomHub) get(name string) *roomEntry { e, ok := h.rooms[name] if !ok { e = &roomEntry{targets: map[string]*gen.StreamTarget{}, subs: map[chan []byte]struct{}{}} h.rooms[name] = e } return e } func (h *roomHub) setTarget(room, backend string, t *gen.StreamTarget) { h.mu.Lock() defer h.mu.Unlock() e := h.get(room) e.targets[backend] = t h.broadcast(room, e) } func (h *roomHub) removeTarget(room, backend string) { h.mu.Lock() defer h.mu.Unlock() e, ok := h.rooms[room] if !ok { return } delete(e.targets, backend) if len(e.targets) == 0 && len(e.subs) == 0 { delete(h.rooms, room) return } h.broadcast(room, e) } 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) } 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, }) for ch := range e.subs { select { case ch <- payload: default: } } }