Files

162 lines
5.1 KiB
Go

// ABOUTME: PlayerGroupRole handles player stream setup on client join
// ABOUTME: Codec negotiation, encoder creation, and stream/start message
package sendspin
import (
"encoding/base64"
"fmt"
"log"
"github.com/Sendspin/sendspin-go/internal/server"
"github.com/Sendspin/sendspin-go/pkg/audio"
"github.com/Sendspin/sendspin-go/pkg/protocol"
)
// PlayerRoleConfig configures the PlayerGroupRole.
type PlayerRoleConfig struct {
SampleRate int
Channels int
BitDepth int
// NewEncoder creates an Opus encoder when needed. When nil, Opus
// clients fall back to PCM. Typically set to server.NewOpusEncoder.
NewEncoder func(sampleRate, channels, chunkSamples int) (*server.OpusEncoder, error)
// NewFLACEncoder creates a FLAC encoder when needed. When nil, FLAC
// clients fall back to PCM.
NewFLACEncoder func(sampleRate, channels, bitDepth, blockSize int) (*server.FLACEncoder, error)
}
// PlayerGroupRole handles the "player" role family. On client join it
// negotiates a codec, creates any needed encoder/resampler, and sends
// stream/start. The audio chunk pipeline (generateAndSendChunk) stays
// on Server — this role only handles the per-client setup.
type PlayerGroupRole struct {
config PlayerRoleConfig
}
// NewPlayerRole creates a PlayerGroupRole with the given config.
func NewPlayerRole(config PlayerRoleConfig) *PlayerGroupRole {
return &PlayerGroupRole{config: config}
}
func (r *PlayerGroupRole) Role() string { return "player" }
// OnClientJoin negotiates a codec, creates encoder/resampler if needed,
// and sends stream/start to clients advertising the player role.
func (r *PlayerGroupRole) OnClientJoin(c *ServerClient) {
if !c.HasRole("player") {
return
}
if c.capabilities != nil {
fmts := make([]string, 0, len(c.capabilities.SupportedFormats))
for _, f := range c.capabilities.SupportedFormats {
fmts = append(fmts, fmt.Sprintf("%s@%dHz/%dbit/%dch", f.Codec, f.SampleRate, f.BitDepth, f.Channels))
}
log.Printf("Client %s advertised formats (in order): %v", c.Name(), fmts)
} else {
log.Printf("Client %s advertised no capabilities", c.Name())
}
codec := negotiateCodec(c, r.config.SampleRate)
var opusEncoder *server.OpusEncoder
var flacEncoder *server.FLACEncoder
var resampler *audio.Resampler
flacBitDepth := r.config.BitDepth
switch codec {
case "opus":
if r.config.SampleRate != 48000 {
resampler = audio.NewResampler(r.config.SampleRate, 48000, r.config.Channels)
log.Printf("Created resampler: %dHz -> 48kHz for Opus (client: %s)", r.config.SampleRate, c.Name())
}
opusChunkSamples := (48000 * ChunkDurationMs) / 1000
if r.config.NewEncoder != nil {
encoder, err := r.config.NewEncoder(48000, r.config.Channels, opusChunkSamples)
if err != nil {
log.Printf("Failed to create Opus encoder for %s, falling back to PCM: %v", c.Name(), err)
codec = "pcm"
resampler = nil
} else {
opusEncoder = encoder
}
} else {
log.Printf("No Opus encoder factory for %s, falling back to PCM", c.Name())
codec = "pcm"
resampler = nil
}
case "flac":
if c.capabilities != nil {
for _, format := range c.capabilities.SupportedFormats {
if format.Codec == "flac" && format.SampleRate == r.config.SampleRate {
flacBitDepth = format.BitDepth
break
}
}
}
chunkSamples := (r.config.SampleRate * ChunkDurationMs) / 1000
if r.config.NewFLACEncoder != nil {
encoder, err := r.config.NewFLACEncoder(r.config.SampleRate, r.config.Channels, flacBitDepth, chunkSamples)
if err != nil {
log.Printf("Failed to create FLAC encoder for %s, falling back to PCM: %v", c.Name(), err)
codec = "pcm"
} else {
flacEncoder = encoder
log.Printf("FLAC encoder for %s: %dHz/%dbit/%dch", c.Name(), r.config.SampleRate, flacBitDepth, r.config.Channels)
}
} else {
log.Printf("No FLAC encoder factory for %s, falling back to PCM", c.Name())
codec = "pcm"
}
}
// Create buffer tracker from client's advertised buffer_capacity.
bufferCapacity := 1048576 // 1MB default
if c.capabilities != nil && c.capabilities.BufferCapacity > 0 {
bufferCapacity = c.capabilities.BufferCapacity
}
c.mu.Lock()
c.codec = codec
c.opusEncoder = opusEncoder
c.flacEncoder = flacEncoder
c.resampler = resampler
c.bufferTracker = NewBufferTracker(bufferCapacity)
c.mu.Unlock()
log.Printf("Added client %s with codec %s", c.Name(), codec)
streamSampleRate := r.config.SampleRate
streamBitDepth := r.config.BitDepth
if codec == "opus" {
streamSampleRate = 48000
streamBitDepth = 16
} else if codec == "flac" {
streamBitDepth = flacBitDepth
}
var codecHeaderB64 string
if codec == "flac" && flacEncoder != nil {
codecHeaderB64 = base64.StdEncoding.EncodeToString(flacEncoder.CodecHeader())
}
streamStart := protocol.StreamStart{
Player: &protocol.StreamStartPlayer{
Codec: codec,
SampleRate: streamSampleRate,
Channels: r.config.Channels,
BitDepth: streamBitDepth,
CodecHeader: codecHeaderB64,
},
}
if err := c.Send("stream/start", streamStart); err != nil {
log.Printf("Error sending stream/start to %s: %v", c.Name(), err)
}
}
func (r *PlayerGroupRole) OnClientLeave(id string, name string) {}