🎉 live server seems to be working now
This commit is contained in:
145
third_party/sendspin-go/pkg/protocol/server_conn.go
vendored
Normal file
145
third_party/sendspin-go/pkg/protocol/server_conn.go
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
// ABOUTME: ServerConn wraps a WebSocket for server-side typed message sending
|
||||
// ABOUTME: CGO-free alternative to sendspin.ServerClient for adapters and tools
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ServerConn wraps an accepted WebSocket connection with typed Send and
|
||||
// SendBinary methods plus a background writer goroutine. It is the
|
||||
// lightweight, CGO-free counterpart to sendspin.ServerClient — use it
|
||||
// when you need typed protocol I/O without the full server runtime
|
||||
// (e.g., conformance adapters, CLI tools, test harnesses).
|
||||
//
|
||||
// The zero value is not usable — construct via NewServerConn.
|
||||
type ServerConn struct {
|
||||
conn *websocket.Conn
|
||||
id string
|
||||
name string
|
||||
sendChan chan interface{}
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// NewServerConn wraps an existing WebSocket connection and starts a
|
||||
// background writer goroutine. Call Close() when done to stop the
|
||||
// writer. Does NOT close the underlying WebSocket — the caller owns
|
||||
// that lifecycle.
|
||||
func NewServerConn(conn *websocket.Conn, id, name string) *ServerConn {
|
||||
sc := &ServerConn{
|
||||
conn: conn,
|
||||
id: id,
|
||||
name: name,
|
||||
sendChan: make(chan interface{}, 100),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go sc.runWriter()
|
||||
return sc
|
||||
}
|
||||
|
||||
// ID returns the identifier passed to NewServerConn.
|
||||
func (sc *ServerConn) ID() string { return sc.id }
|
||||
|
||||
// Name returns the name passed to NewServerConn.
|
||||
func (sc *ServerConn) Name() string { return sc.name }
|
||||
|
||||
// Send enqueues a typed control message (JSON envelope) for
|
||||
// transmission. Returns an error immediately if the send buffer is
|
||||
// full. The message is wrapped in a {"type": msgType, "payload": ...}
|
||||
// envelope by the writer goroutine.
|
||||
func (sc *ServerConn) Send(msgType string, payload interface{}) error {
|
||||
msg := Message{
|
||||
Type: msgType,
|
||||
Payload: payload,
|
||||
}
|
||||
select {
|
||||
case sc.sendChan <- msg:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("send buffer full")
|
||||
}
|
||||
}
|
||||
|
||||
// SendBinary enqueues a raw binary frame for transmission. Returns an
|
||||
// error immediately if the send buffer is full.
|
||||
func (sc *ServerConn) SendBinary(data []byte) error {
|
||||
select {
|
||||
case sc.sendChan <- data:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("send buffer full")
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the writer goroutine. Safe to call multiple times. Does
|
||||
// NOT close the underlying WebSocket connection.
|
||||
func (sc *ServerConn) Close() {
|
||||
sc.once.Do(func() {
|
||||
close(sc.done)
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *ServerConn) runWriter() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
const writeDeadline = 10 * time.Second
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-sc.sendChan:
|
||||
switch v := msg.(type) {
|
||||
case []byte:
|
||||
sc.conn.SetWriteDeadline(time.Now().Add(writeDeadline))
|
||||
if err := sc.conn.WriteMessage(websocket.BinaryMessage, v); err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sc.conn.SetWriteDeadline(time.Now().Add(writeDeadline))
|
||||
if err := sc.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
if err := sc.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case <-sc.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAudioChunk packs timestamp + payload into a Sendspin binary audio
|
||||
// frame: [1 byte message type][8 byte big-endian timestamp (µs)][audio bytes].
|
||||
func CreateAudioChunk(timestamp int64, audioData []byte) []byte {
|
||||
chunk := make([]byte, BinaryMessageHeaderSize+len(audioData))
|
||||
chunk[0] = AudioChunkMessageType
|
||||
binary.BigEndian.PutUint64(chunk[1:BinaryMessageHeaderSize], uint64(timestamp))
|
||||
copy(chunk[BinaryMessageHeaderSize:], audioData)
|
||||
return chunk
|
||||
}
|
||||
|
||||
// CreateArtworkChunk packs an artwork frame: [1 byte message type][8 byte
|
||||
// timestamp (µs)][image bytes]. Channel is 0-3, mapping to the artwork
|
||||
// channel message types (8-11).
|
||||
func CreateArtworkChunk(channel int, timestamp int64, imageData []byte) []byte {
|
||||
chunk := make([]byte, BinaryMessageHeaderSize+len(imageData))
|
||||
chunk[0] = byte(ArtworkChannel0MessageType + channel)
|
||||
binary.BigEndian.PutUint64(chunk[1:BinaryMessageHeaderSize], uint64(timestamp))
|
||||
copy(chunk[BinaryMessageHeaderSize:], imageData)
|
||||
return chunk
|
||||
}
|
||||
Reference in New Issue
Block a user