package factory import ( "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" "aoiprxkit" "github.com/jan/fm-rds-tx/internal/config" "github.com/jan/fm-rds-tx/internal/ingest" "github.com/jan/fm-rds-tx/internal/ingest/adapters/aoip" "github.com/jan/fm-rds-tx/internal/ingest/adapters/httpraw" "github.com/jan/fm-rds-tx/internal/ingest/adapters/icecast" "github.com/jan/fm-rds-tx/internal/ingest/adapters/srt" "github.com/jan/fm-rds-tx/internal/ingest/adapters/stdinpcm" ) type Deps struct { Stdin io.Reader HTTP *http.Client SRTOpener aoiprxkit.SRTConnOpener AES67ReceiverFactory aoip.ReceiverFactory } type AudioIngress interface { WritePCM16(data []byte) (int, error) } func BuildSource(cfg config.Config, deps Deps) (ingest.Source, AudioIngress, error) { switch normalizeIngestKind(cfg.Ingest.Kind) { case "", "none": return nil, nil, nil case "stdin", "stdin-pcm": reader := deps.Stdin if reader == nil { reader = os.Stdin } src := stdinpcm.New("stdin-main", reader, cfg.Ingest.Stdin.SampleRateHz, cfg.Ingest.Stdin.Channels, 1024) return src, nil, nil case "http-raw": src := httpraw.New("http-raw-main", cfg.Ingest.HTTPRaw.SampleRateHz, cfg.Ingest.HTTPRaw.Channels) return src, src, nil case "icecast": src := icecast.New( "icecast-main", cfg.Ingest.Icecast.URL, deps.HTTP, icecast.ReconnectConfig{ Enabled: cfg.Ingest.Reconnect.Enabled, InitialBackoffMs: cfg.Ingest.Reconnect.InitialBackoffMs, MaxBackoffMs: cfg.Ingest.Reconnect.MaxBackoffMs, }, icecast.WithDecoderPreference(cfg.Ingest.Icecast.Decoder), ) return src, nil, nil case "srt": srtCfg := aoiprxkit.SRTConfig{ URL: cfg.Ingest.SRT.URL, Mode: cfg.Ingest.SRT.Mode, SampleRateHz: cfg.Ingest.SRT.SampleRateHz, Channels: cfg.Ingest.SRT.Channels, } opts := []srt.Option{} if deps.SRTOpener != nil { opts = append(opts, srt.WithConnOpener(deps.SRTOpener)) } src := srt.New("srt-main", srtCfg, opts...) return src, nil, nil case "aes67", "aoip", "aoip-rtp": aoipCfg, err := buildAES67Config(cfg) if err != nil { return nil, nil, err } opts := []aoip.Option{} if deps.AES67ReceiverFactory != nil { opts = append(opts, aoip.WithReceiverFactory(deps.AES67ReceiverFactory)) } src := aoip.New("aes67-main", aoipCfg, opts...) return src, nil, nil default: return nil, nil, fmt.Errorf("unsupported ingest kind: %s", cfg.Ingest.Kind) } } func SampleRateForKind(cfg config.Config) int { switch normalizeIngestKind(cfg.Ingest.Kind) { case "stdin", "stdin-pcm": if cfg.Ingest.Stdin.SampleRateHz > 0 { return cfg.Ingest.Stdin.SampleRateHz } case "http-raw": if cfg.Ingest.HTTPRaw.SampleRateHz > 0 { return cfg.Ingest.HTTPRaw.SampleRateHz } case "icecast": return 44100 case "srt": if cfg.Ingest.SRT.SampleRateHz > 0 { return cfg.Ingest.SRT.SampleRateHz } case "aes67", "aoip", "aoip-rtp": if cfg.Ingest.AES67.SampleRateHz > 0 { return cfg.Ingest.AES67.SampleRateHz } } return 44100 } func normalizeIngestKind(kind string) string { return strings.ToLower(strings.TrimSpace(kind)) } func buildAES67Config(cfg config.Config) (aoiprxkit.Config, error) { base := aoiprxkit.DefaultConfig() ing := cfg.Ingest.AES67 if strings.TrimSpace(ing.InterfaceName) != "" { base.InterfaceName = strings.TrimSpace(ing.InterfaceName) } if ing.PayloadType >= 0 { base.PayloadType = uint8(ing.PayloadType) } if ing.SampleRateHz > 0 { base.SampleRateHz = ing.SampleRateHz } if ing.Channels > 0 { base.Channels = ing.Channels } if strings.TrimSpace(ing.Encoding) != "" { base.Encoding = strings.ToUpper(strings.TrimSpace(ing.Encoding)) } if ing.PacketTimeMs > 0 { base.PacketTime = time.Duration(ing.PacketTimeMs) * time.Millisecond } if ing.JitterDepthPackets > 0 { base.JitterDepthPackets = ing.JitterDepthPackets } if ing.ReadBufferBytes > 0 { base.ReadBufferBytes = ing.ReadBufferBytes } sdpText := strings.TrimSpace(ing.SDP) if sdpText == "" && strings.TrimSpace(ing.SDPPath) != "" { data, err := os.ReadFile(filepath.Clean(ing.SDPPath)) if err != nil { return aoiprxkit.Config{}, fmt.Errorf("read ingest.aes67.sdpPath: %w", err) } sdpText = string(data) } if sdpText != "" { info, err := aoiprxkit.ParseMinimalSDP(sdpText) if err != nil { return aoiprxkit.Config{}, fmt.Errorf("parse ingest.aes67 SDP: %w", err) } parsed, err := aoiprxkit.ConfigFromSDP(base, info) if err != nil { return aoiprxkit.Config{}, fmt.Errorf("map ingest.aes67 SDP: %w", err) } return parsed, nil } if strings.TrimSpace(ing.MulticastGroup) != "" { base.MulticastGroup = strings.TrimSpace(ing.MulticastGroup) } if ing.Port > 0 { base.Port = ing.Port } if err := base.Validate(); err != nil { return aoiprxkit.Config{}, err } return base, nil }