feat(client): implement server-initiated mode with WebSocket support

This commit is contained in:
2026-06-14 12:47:30 +02:00
parent 1054d4b9d0
commit a8c9bdbffb
4 changed files with 246 additions and 47 deletions

View File

@@ -14,6 +14,7 @@ import (
"github.com/Sendspin/sendspin-go/pkg/audio/output"
"github.com/Sendspin/sendspin-go/pkg/protocol"
"github.com/Sendspin/sendspin-go/pkg/sync"
"github.com/gorilla/websocket"
)
// ReconnectConfig controls automatic reconnect behavior after the protocol
@@ -216,6 +217,55 @@ func (p *Player) Connect() error {
return nil
}
// Accept drives the player over a server-initiated WebSocket connection
// (the server discovered this player via mDNS and dialed it). It builds a
// receiver over the accepted conn, starts audio consumption, and BLOCKS
// until the connection drops or the player is closed — so callers running a
// listener can serialize one session at a time and re-accept on return.
//
// Unlike Connect, Accept does not start the dial-based reconnect loop: in
// server-initiated mode the server redials, so reconnection is the listener's
// responsibility, not the player's. The caller transfers ownership of conn.
func (p *Player) Accept(conn *websocket.Conn) error {
recv, err := p.buildReceiver("server-initiated")
if err != nil {
return err
}
if err := recv.AcceptConn(conn); err != nil {
return err
}
p.receiver = recv
p.state.Connected = true
p.notifyStateChange()
go p.consumeAudio(recv)
// Block for the lifetime of this connection so the caller's accept loop
// serializes sessions (one server connection at a time).
select {
case <-recv.Done():
case <-p.ctx.Done():
}
recv.Close()
p.state.Connected = false
p.state.State = "idle"
p.notifyStateChange()
return nil
}
// CloseConnection drops the currently active receiver, if any, without
// tearing down the Player or its audio output. In server-initiated mode a
// listener calls this to evict a stale/superseded session before accepting
// a newer connection; the blocked Accept for the old session then returns.
func (p *Player) CloseConnection() {
if p.receiver != nil {
p.receiver.Close()
}
}
func (p *Player) buildReceiver(addr string) (*Receiver, error) {
p.ensureCapsResolved()
return NewReceiver(ReceiverConfig{