201 lines
5.2 KiB
Go
201 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/Sendspin/sendspin-go/pkg/sendspin"
|
|
|
|
"rpi-sendspin/internal/live"
|
|
)
|
|
|
|
// flagSpec describes one user-facing option. The flag is registered
|
|
// under all names; help output (custom Usage below) groups them onto a
|
|
// single line so `-s, --source` reads as one option rather than two.
|
|
type flagSpec struct {
|
|
long string
|
|
short string // optional
|
|
usage string
|
|
}
|
|
|
|
var specs []flagSpec
|
|
|
|
func addStr(target *string, def, long, short, usage string) {
|
|
flag.StringVar(target, long, def, usage)
|
|
if short != "" {
|
|
flag.StringVar(target, short, def, usage)
|
|
}
|
|
specs = append(specs, flagSpec{long, short, usage})
|
|
}
|
|
|
|
func addInt(target *int, def int, long, short, usage string) {
|
|
flag.IntVar(target, long, def, usage)
|
|
if short != "" {
|
|
flag.IntVar(target, short, def, usage)
|
|
}
|
|
specs = append(specs, flagSpec{long, short, usage})
|
|
}
|
|
|
|
func main() {
|
|
var (
|
|
port int
|
|
name string
|
|
httpPort int
|
|
configPath string
|
|
source string
|
|
mqttURL string
|
|
mqttUser string
|
|
mqttPass string
|
|
mqttBase string
|
|
mqttDiscovery string
|
|
rate int
|
|
channels int
|
|
)
|
|
|
|
addInt(&port, 8927, "port", "p", "Sendspin WebSocket port")
|
|
addStr(&name, "Live Audio", "name", "n", "advertised server name")
|
|
addInt(&httpPort, 8080, "http-port", "", "HTTP/API port")
|
|
addStr(&configPath, defaultConfigPath(), "config", "c", "presets file path")
|
|
addStr(&source, "", "source", "s", "PipeWire source node (e.g. alsa_input.usb-...pro-input-0.monitor); when set, overrides preset.source")
|
|
addStr(&mqttURL, "", "mqtt-url", "", "MQTT broker URL (e.g. tcp://broker.local:1883); empty disables MQTT")
|
|
addStr(&mqttUser, "", "mqtt-username", "", "MQTT username")
|
|
addStr(&mqttPass, "", "mqtt-password", "", "MQTT password")
|
|
addStr(&mqttBase, "sendspin/live", "mqtt-base", "", "MQTT base topic")
|
|
addStr(&mqttDiscovery, "homeassistant", "mqtt-discovery", "", "Home Assistant discovery prefix")
|
|
addInt(&rate, 48000, "rate", "r", "capture sample rate")
|
|
addInt(&channels, 2, "channels", "", "capture channels")
|
|
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
|
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
|
|
|
store, err := live.NewStore(configPath)
|
|
if err != nil {
|
|
log.Fatalf("load presets: %v", err)
|
|
}
|
|
|
|
src := live.NewSource(rate, channels)
|
|
roster := live.NewRoster()
|
|
|
|
var mgr *live.Manager
|
|
server, err := sendspin.NewServer(sendspin.ServerConfig{
|
|
Port: port,
|
|
Name: name,
|
|
Source: src,
|
|
EnableMDNS: true,
|
|
DiscoverClients: true,
|
|
ClientFilter: func(id string) bool {
|
|
if mgr == nil {
|
|
return false
|
|
}
|
|
return mgr.ClientAllowed(id)
|
|
},
|
|
OnClientHello: roster.Record,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("create sendspin server: %v", err)
|
|
}
|
|
mgr = live.NewManager(store, server, src)
|
|
mgr.SetSourceOverride(source)
|
|
if source != "" {
|
|
log.Printf("source override: %s (preset.source ignored)", source)
|
|
}
|
|
|
|
disc := live.NewDiscovery()
|
|
disc.Start()
|
|
defer disc.Stop()
|
|
|
|
httpSrv := &http.Server{
|
|
Addr: ":" + strconv.Itoa(httpPort),
|
|
Handler: live.NewHTTPServer(mgr, store, disc, roster, server).Handler(),
|
|
ReadTimeout: 5 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
}
|
|
go func() {
|
|
log.Printf("HTTP API listening on %s", httpSrv.Addr)
|
|
if err := httpSrv.ListenAndServe(); err != http.ErrServerClosed {
|
|
log.Fatalf("http: %v", err)
|
|
}
|
|
}()
|
|
|
|
var bridge *live.MQTTBridge
|
|
if mqttURL != "" {
|
|
bridge = live.NewMQTTBridge(live.MQTTConfig{
|
|
URL: mqttURL,
|
|
Username: mqttUser,
|
|
Password: mqttPass,
|
|
BaseTopic: mqttBase,
|
|
DiscoveryPrefix: mqttDiscovery,
|
|
}, mgr)
|
|
if err := bridge.Start(); err != nil {
|
|
log.Fatalf("mqtt: %v", err)
|
|
}
|
|
}
|
|
|
|
serverDone := make(chan error, 1)
|
|
go func() { serverDone <- server.Start() }()
|
|
log.Printf("Sendspin server %q on :%d", name, port)
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
|
select {
|
|
case <-sig:
|
|
log.Printf("signal received, shutting down")
|
|
case err := <-serverDone:
|
|
log.Printf("sendspin server exited: %v", err)
|
|
}
|
|
|
|
if bridge != nil {
|
|
bridge.Stop()
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = httpSrv.Shutdown(ctx)
|
|
server.Stop()
|
|
_ = src.Close()
|
|
}
|
|
|
|
func defaultConfigPath() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "presets.json"
|
|
}
|
|
return filepath.Join(home, ".config", "sendspin-live", "presets.json")
|
|
}
|
|
|
|
// usage renders one line per logical option, pairing the short alias
|
|
// with the long form (e.g. "-s, --source"). The default flag.Usage
|
|
// would list each registration separately and double-print everything.
|
|
func usage() {
|
|
out := flag.CommandLine.Output()
|
|
fmt.Fprintf(out, "Usage: %s [flags]\n\nFlags:\n", os.Args[0])
|
|
|
|
sort.Slice(specs, func(i, j int) bool { return specs[i].long < specs[j].long })
|
|
|
|
for _, s := range specs {
|
|
f := flag.Lookup(s.long)
|
|
if f == nil {
|
|
continue
|
|
}
|
|
head := " --" + s.long
|
|
if s.short != "" {
|
|
head = " -" + s.short + ", --" + s.long
|
|
}
|
|
if f.DefValue != "" {
|
|
head += "=" + f.DefValue
|
|
}
|
|
fmt.Fprintf(out, "%-44s %s\n", head, s.usage)
|
|
}
|
|
}
|