This commit is contained in:
2026-06-15 22:46:12 +08:00
commit f6508eccdb
38 changed files with 3133 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
package room
import "time"
const (
PlaybackIdle = "idle"
PlaybackPlaying = "playing"
PlaybackPaused = "paused"
SourceModeDirect = "direct"
SourceModeProxy = "proxy"
)
type Room struct {
Code string `json:"code"`
OwnerDeviceID string `json:"ownerDeviceId,omitempty"`
OwnerLastSeenAt int64 `json:"ownerLastSeenAt,omitempty"`
OnlineCount int `json:"onlineCount"`
Source *Source `json:"source,omitempty"`
Playback Playback `json:"playback"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
ExpiresAt int64 `json:"expiresAt"`
}
type Source struct {
Type string `json:"type"`
Mode string `json:"mode"`
URL string `json:"url"`
IsLive bool `json:"isLive"`
Headers map[string]string `json:"headers,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
ProxyPath string `json:"proxyPath,omitempty"`
}
type Playback struct {
State string `json:"state"`
PositionMS int64 `json:"positionMs"`
ServerUpdatedAt int64 `json:"serverUpdatedAt"`
TargetLatencyMS int64 `json:"targetLatencyMs,omitempty"`
}
type Snapshot struct {
Room Room `json:"room"`
IsOwner bool `json:"isOwner"`
DeviceID string `json:"deviceId"`
ServerNow int64 `json:"serverNow"`
}
func New(code string, ttl time.Duration) Room {
now := time.Now()
return Room{
Code: code,
Playback: Playback{State: PlaybackIdle, ServerUpdatedAt: now.UnixMilli()},
CreatedAt: now.UnixMilli(),
UpdatedAt: now.UnixMilli(),
ExpiresAt: now.Add(ttl).UnixMilli(),
OnlineCount: 0,
}
}
func (r Room) PublicFor(deviceID string) Room {
out := r
if out.Source != nil {
src := *out.Source
if src.Mode == SourceModeProxy {
src.URL = src.ProxyPath
src.Headers = nil
src.Username = ""
src.Password = ""
}
out.Source = &src
}
return out
}
func NowMS() int64 {
return time.Now().UnixMilli()
}

View File

@@ -0,0 +1,139 @@
package room
import (
"context"
"crypto/rand"
"errors"
"math/big"
"strings"
"time"
)
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
type Store interface {
CreateRoom(ctx context.Context, r Room) error
GetRoom(ctx context.Context, code string) (Room, error)
SaveRoom(ctx context.Context, r Room) error
}
type Service struct {
store Store
codeLength int
emptyTTL time.Duration
ownerReconnectHold time.Duration
}
func NewService(st Store, codeLength int, emptyTTL, ownerReconnectHold time.Duration) *Service {
return &Service{
store: st,
codeLength: codeLength,
emptyTTL: emptyTTL,
ownerReconnectHold: ownerReconnectHold,
}
}
func (s *Service) Create(ctx context.Context) (Room, error) {
for i := 0; i < 10; i++ {
code, err := randomCode(s.codeLength)
if err != nil {
return Room{}, err
}
r := New(code, s.emptyTTL)
if err := s.store.CreateRoom(ctx, r); err == nil {
return r, nil
}
}
return Room{}, errors.New("failed to allocate room code")
}
func (s *Service) Join(ctx context.Context, code, deviceID string, onlineCount int) (Room, bool, error) {
code = strings.ToUpper(strings.TrimSpace(code))
if code == "" || deviceID == "" {
return Room{}, false, errors.New("room code and device id are required")
}
r, err := s.store.GetRoom(ctx, code)
if err != nil {
return Room{}, false, err
}
now := NowMS()
if r.OwnerDeviceID == "" {
r.OwnerDeviceID = deviceID
}
if r.OwnerDeviceID == deviceID {
r.OwnerLastSeenAt = now
}
r.OnlineCount = onlineCount
if err := s.store.SaveRoom(ctx, r); err != nil {
return Room{}, false, err
}
return r, r.OwnerDeviceID == deviceID, nil
}
func (s *Service) Get(ctx context.Context, code string) (Room, error) {
return s.store.GetRoom(ctx, strings.ToUpper(strings.TrimSpace(code)))
}
func (s *Service) SetOnlineCount(ctx context.Context, code string, count int) error {
r, err := s.Get(ctx, code)
if err != nil {
return err
}
r.OnlineCount = count
return s.store.SaveRoom(ctx, r)
}
func (s *Service) SetSource(ctx context.Context, code, deviceID string, src Source) (Room, error) {
r, err := s.Get(ctx, code)
if err != nil {
return Room{}, err
}
if r.OwnerDeviceID != deviceID {
return Room{}, errors.New("only room owner can configure source")
}
if src.Mode == "" {
src.Mode = SourceModeDirect
}
if src.Mode != SourceModeDirect && src.Mode != SourceModeProxy {
return Room{}, errors.New("source mode must be direct or proxy")
}
if strings.TrimSpace(src.URL) == "" {
return Room{}, errors.New("source url is required")
}
if src.Mode == SourceModeProxy {
src.ProxyPath = "/api/rooms/" + r.Code + "/stream"
}
r.Source = &src
r.Playback = Playback{State: PlaybackIdle, ServerUpdatedAt: NowMS()}
if err := s.store.SaveRoom(ctx, r); err != nil {
return Room{}, err
}
return r, nil
}
func (s *Service) UpdatePlayback(ctx context.Context, code string, p Playback) (Room, error) {
r, err := s.Get(ctx, code)
if err != nil {
return Room{}, err
}
p.ServerUpdatedAt = NowMS()
r.Playback = p
if err := s.store.SaveRoom(ctx, r); err != nil {
return Room{}, err
}
return r, nil
}
func randomCode(n int) (string, error) {
var b strings.Builder
b.Grow(n)
for i := 0; i < n; i++ {
x, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
if err != nil {
return "", err
}
b.WriteByte(alphabet[x.Int64()])
}
return b.String(), nil
}