🎉 live server seems to be working now
This commit is contained in:
240
third_party/sendspin-go/pkg/sendspin/server_client.go
vendored
Normal file
240
third_party/sendspin-go/pkg/sendspin/server_client.go
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
// ABOUTME: ServerClient represents one accepted connection on a sendspin Server
|
||||
// ABOUTME: Exposes ID/Name/Roles/Send surface for the Group/GroupRole layer
|
||||
package sendspin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sendspin/sendspin-go/internal/server"
|
||||
"github.com/Sendspin/sendspin-go/pkg/audio"
|
||||
"github.com/Sendspin/sendspin-go/pkg/protocol"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ServerClient represents one accepted WebSocket connection on a Server.
|
||||
// It owns the client's negotiated roles, capabilities, playback state,
|
||||
// per-codec encoder state, and the outbound send channel.
|
||||
//
|
||||
// Exposed accessors (ID, Name, Roles, HasRole, Send, SendBinary) are the
|
||||
// stable surface the future Group/GroupRole layer depends on. Internal
|
||||
// fields remain unexported so the server package can mutate them through
|
||||
// the mutex without leaking that detail to callers.
|
||||
type ServerClient struct {
|
||||
id string
|
||||
name string
|
||||
conn *websocket.Conn
|
||||
roles []string
|
||||
capabilities *protocol.PlayerV1Support
|
||||
|
||||
state string
|
||||
volume int
|
||||
muted bool
|
||||
|
||||
codec string
|
||||
opusEncoder *server.OpusEncoder
|
||||
flacEncoder *server.FLACEncoder
|
||||
resampler *audio.Resampler // non-nil only when source rate != 48kHz
|
||||
bufferTracker *BufferTracker
|
||||
|
||||
sendChan chan interface{}
|
||||
done chan struct{}
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// ID returns the client-supplied unique identifier from client/hello.
|
||||
func (c *ServerClient) ID() string { return c.id }
|
||||
|
||||
// Name returns the human-friendly name from client/hello.
|
||||
func (c *ServerClient) Name() string { return c.name }
|
||||
|
||||
// Roles returns the client's advertised role list as a fresh slice.
|
||||
// Callers may mutate the returned slice without affecting the ServerClient.
|
||||
func (c *ServerClient) Roles() []string {
|
||||
out := make([]string, len(c.roles))
|
||||
copy(out, c.roles)
|
||||
return out
|
||||
}
|
||||
|
||||
// HasRole reports whether this client advertised a given role family.
|
||||
// It matches both exact ("player") and versioned ("player@v1") forms so
|
||||
// callers can query by family without tracking versions.
|
||||
func (c *ServerClient) HasRole(role string) bool {
|
||||
for _, r := range c.roles {
|
||||
if r == role || strings.HasPrefix(r, role+"@") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Send enqueues a typed control message for transmission. Returns an
|
||||
// error immediately if the client's send buffer is full rather than
|
||||
// blocking — callers decide whether to drop or disconnect.
|
||||
//
|
||||
// A nil return means the message was enqueued, not that it was delivered:
|
||||
// after the client's writer goroutine has exited (e.g., post-disconnect),
|
||||
// enqueued messages are dropped silently. Callers should treat this as
|
||||
// best-effort once they've observed a client leaving.
|
||||
func (c *ServerClient) Send(msgType string, payload interface{}) error {
|
||||
msg := protocol.Message{
|
||||
Type: msgType,
|
||||
Payload: payload,
|
||||
}
|
||||
select {
|
||||
case c.sendChan <- msg:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("client send buffer full")
|
||||
}
|
||||
}
|
||||
|
||||
// SendBinary enqueues a raw binary frame (e.g., an audio chunk) for
|
||||
// transmission. Same non-blocking semantics as Send.
|
||||
//
|
||||
// A nil return means the frame was enqueued, not that it was delivered:
|
||||
// after the client's writer goroutine has exited (e.g., post-disconnect),
|
||||
// enqueued frames are dropped silently. Callers should treat this as
|
||||
// best-effort once they've observed a client leaving.
|
||||
func (c *ServerClient) SendBinary(data []byte) error {
|
||||
select {
|
||||
case c.sendChan <- data:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("client send buffer full (depth=%d, cap=%d)",
|
||||
len(c.sendChan), cap(c.sendChan))
|
||||
}
|
||||
}
|
||||
|
||||
// State returns the client's current playback state ("synchronized",
|
||||
// "playing", "paused"). Safe to call concurrently with state updates.
|
||||
func (c *ServerClient) State() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.state
|
||||
}
|
||||
|
||||
// Volume returns the client's reported volume (0-100). Safe to call
|
||||
// concurrently with state updates.
|
||||
func (c *ServerClient) Volume() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.volume
|
||||
}
|
||||
|
||||
// Muted returns the client's mute state. Safe to call concurrently with
|
||||
// state updates.
|
||||
func (c *ServerClient) Muted() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.muted
|
||||
}
|
||||
|
||||
// Codec returns the currently-negotiated codec name ("pcm", "opus", "").
|
||||
// Returns the empty string before stream negotiation completes. Safe to
|
||||
// call concurrently with state updates.
|
||||
func (c *ServerClient) Codec() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.codec
|
||||
}
|
||||
|
||||
// NewServerClientFromConn wraps an existing WebSocket connection in a
|
||||
// ServerClient and starts a background writer goroutine. This is the
|
||||
// entry point for code that accepts its own WebSocket connections
|
||||
// (e.g., conformance adapters) and wants to use the typed Send/SendBinary
|
||||
// API without constructing a full Server.
|
||||
//
|
||||
// The caller is responsible for reading from the connection; the writer
|
||||
// goroutine handles outbound messages via Send/SendBinary. Call Close()
|
||||
// when done to stop the writer and release resources.
|
||||
func NewServerClientFromConn(conn *websocket.Conn, id, name string, roles []string, capabilities *protocol.PlayerV1Support) *ServerClient {
|
||||
c := &ServerClient{
|
||||
id: id,
|
||||
name: name,
|
||||
conn: conn,
|
||||
roles: roles,
|
||||
capabilities: capabilities,
|
||||
state: "synchronized",
|
||||
volume: 100,
|
||||
sendChan: make(chan interface{}, 100),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go c.runWriter()
|
||||
return c
|
||||
}
|
||||
|
||||
// runWriter drains the sendChan and writes messages to the WebSocket.
|
||||
// Exits when the done channel is closed (via Close). This is the
|
||||
// standalone equivalent of Server.clientWriter for ServerClients
|
||||
// created via NewServerClientFromConn.
|
||||
func (c *ServerClient) runWriter() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
const writeDeadline = 10 * time.Second
|
||||
|
||||
exit := func(reason string, err error) {
|
||||
log.Printf("runWriter %s exiting: %s: %v (queue depth %d)",
|
||||
c.name, reason, err, len(c.sendChan))
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-c.sendChan:
|
||||
queueDepth := len(c.sendChan)
|
||||
switch v := msg.(type) {
|
||||
case []byte:
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeDeadline))
|
||||
wStart := time.Now()
|
||||
if err := c.conn.WriteMessage(websocket.BinaryMessage, v); err != nil {
|
||||
exit("binary write", err)
|
||||
return
|
||||
}
|
||||
wDur := time.Since(wStart)
|
||||
if wDur > 5*time.Millisecond || queueDepth > 5 {
|
||||
log.Printf("ws write %s: %s for %d bytes (queue depth %d)",
|
||||
c.name, wDur.Round(time.Microsecond), len(v), queueDepth)
|
||||
}
|
||||
default:
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeDeadline))
|
||||
if err := c.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
exit("text write", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
if err := c.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
|
||||
exit("ping", err)
|
||||
return
|
||||
}
|
||||
|
||||
case <-c.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the writer goroutine and signals that this ServerClient
|
||||
// is done. Safe to call multiple times. Does NOT close the underlying
|
||||
// WebSocket connection — the caller owns that lifecycle.
|
||||
func (c *ServerClient) Close() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
select {
|
||||
case <-c.done:
|
||||
// Already closed
|
||||
default:
|
||||
close(c.done)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user