🎉 live server seems to be working now
This commit is contained in:
141
internal/live/source.go
Normal file
141
internal/live/source.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"rpi-sendspin/internal/pipewire"
|
||||
)
|
||||
|
||||
// Source is the sendspin AudioSource used by the live server. It wraps
|
||||
// a single pipewire.Source instance and exposes runtime control:
|
||||
//
|
||||
// - SetTarget swaps the captured PipeWire node and recreates the
|
||||
// underlying pw-cat process.
|
||||
// - SetActive(false) makes Read() return (0, nil) on every call so the
|
||||
// sendspin audio engine skips its tick without surfacing an error.
|
||||
// pw-cat is killed in this mode so we don't burn CPU capturing audio
|
||||
// no one is listening to.
|
||||
//
|
||||
// The sample rate / channel count are fixed at construction; v1 of the
|
||||
// live server does not retune between presets.
|
||||
type Source struct {
|
||||
sampleRate int
|
||||
channels int
|
||||
|
||||
mu sync.Mutex
|
||||
target string
|
||||
active bool
|
||||
pw *pipewire.Source
|
||||
retryAt time.Time // earliest time the next pw-cat start may be attempted
|
||||
failures int // consecutive failures, drives exponential backoff
|
||||
lastError string // last error message; logged once per change to avoid spam
|
||||
}
|
||||
|
||||
const (
|
||||
backoffMin = 500 * time.Millisecond
|
||||
backoffMax = 10 * time.Second
|
||||
)
|
||||
|
||||
func NewSource(sampleRate, channels int) *Source {
|
||||
return &Source{
|
||||
sampleRate: sampleRate,
|
||||
channels: channels,
|
||||
}
|
||||
}
|
||||
|
||||
// SetTarget sets the PipeWire node to capture from. If the target
|
||||
// changes while the source is active, the underlying pw-cat process is
|
||||
// torn down and recreated on the next Read.
|
||||
func (s *Source) SetTarget(target string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.target == target {
|
||||
return
|
||||
}
|
||||
s.target = target
|
||||
s.failures = 0
|
||||
s.retryAt = time.Time{}
|
||||
s.lastError = ""
|
||||
s.teardownLocked()
|
||||
}
|
||||
|
||||
// SetActive controls whether Read pulls from pw-cat. When set to false
|
||||
// any running pw-cat is killed and subsequent Reads return (0, nil).
|
||||
func (s *Source) SetActive(active bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.active == active {
|
||||
return
|
||||
}
|
||||
s.active = active
|
||||
if !active {
|
||||
s.teardownLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Source) teardownLocked() {
|
||||
if s.pw != nil {
|
||||
_ = s.pw.Close()
|
||||
s.pw = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Source) Read(samples []int32) (int, error) {
|
||||
s.mu.Lock()
|
||||
if !s.active || s.target == "" {
|
||||
s.mu.Unlock()
|
||||
return 0, nil
|
||||
}
|
||||
// Backoff: when pw-cat keeps dying (e.g. invalid target), don't
|
||||
// fork-bomb a new process on every 20 ms tick. Return (0, nil) so
|
||||
// the sendspin audio engine skips the tick silently until retryAt.
|
||||
if !s.retryAt.IsZero() && time.Now().Before(s.retryAt) {
|
||||
s.mu.Unlock()
|
||||
return 0, nil
|
||||
}
|
||||
if s.pw == nil {
|
||||
s.pw = pipewire.NewSource(s.target, s.sampleRate, s.channels)
|
||||
}
|
||||
pw := s.pw
|
||||
target := s.target
|
||||
s.mu.Unlock()
|
||||
|
||||
n, err := pw.Read(samples)
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
s.teardownLocked()
|
||||
s.failures++
|
||||
backoff := backoffMin << (s.failures - 1)
|
||||
if backoff > backoffMax || backoff <= 0 {
|
||||
backoff = backoffMax
|
||||
}
|
||||
s.retryAt = time.Now().Add(backoff)
|
||||
msg := err.Error()
|
||||
if msg != s.lastError {
|
||||
log.Printf("pw-cat[%s] read error: %v (retry in %s)", target, err, backoff)
|
||||
s.lastError = msg
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return 0, nil
|
||||
}
|
||||
if n > 0 {
|
||||
s.mu.Lock()
|
||||
s.failures = 0
|
||||
s.lastError = ""
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (s *Source) SampleRate() int { return s.sampleRate }
|
||||
func (s *Source) Channels() int { return s.channels }
|
||||
func (s *Source) Metadata() (string, string, string) { return "", "", "" }
|
||||
|
||||
func (s *Source) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.teardownLocked()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user