162 lines
5.1 KiB
Go
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) {}
|