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 }