307 lines
7.6 KiB
Go
307 lines
7.6 KiB
Go
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:
|
||
}
|
||
}
|
||
}
|