Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

261 řádky
7.4KB

  1. package factory
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "time"
  11. "aoiprxkit"
  12. "github.com/jan/fm-rds-tx/internal/config"
  13. "github.com/jan/fm-rds-tx/internal/ingest"
  14. "github.com/jan/fm-rds-tx/internal/ingest/adapters/aoip"
  15. "github.com/jan/fm-rds-tx/internal/ingest/adapters/httpraw"
  16. "github.com/jan/fm-rds-tx/internal/ingest/adapters/icecast"
  17. "github.com/jan/fm-rds-tx/internal/ingest/adapters/srt"
  18. "github.com/jan/fm-rds-tx/internal/ingest/adapters/stdinpcm"
  19. )
  20. type Deps struct {
  21. Stdin io.Reader
  22. HTTP *http.Client
  23. SRTOpener aoiprxkit.SRTConnOpener
  24. AES67ReceiverFactory aoip.ReceiverFactory
  25. AES67Discover AES67DiscoverFunc
  26. }
  27. type AudioIngress interface {
  28. WritePCM16(data []byte) (int, error)
  29. }
  30. type AES67DiscoverRequest struct {
  31. StreamName string
  32. Timeout time.Duration
  33. InterfaceName string
  34. SAPGroup string
  35. SAPPort int
  36. }
  37. type AES67DiscoverFunc func(ctx context.Context, req AES67DiscoverRequest) (aoiprxkit.SAPAnnouncement, error)
  38. func BuildSource(cfg config.Config, deps Deps) (ingest.Source, AudioIngress, error) {
  39. switch normalizeIngestKind(cfg.Ingest.Kind) {
  40. case "", "none":
  41. return nil, nil, nil
  42. case "stdin", "stdin-pcm":
  43. reader := deps.Stdin
  44. if reader == nil {
  45. reader = os.Stdin
  46. }
  47. src := stdinpcm.New("stdin-main", reader, cfg.Ingest.Stdin.SampleRateHz, cfg.Ingest.Stdin.Channels, 1024)
  48. return src, nil, nil
  49. case "http-raw":
  50. src := httpraw.New("http-raw-main", cfg.Ingest.HTTPRaw.SampleRateHz, cfg.Ingest.HTTPRaw.Channels)
  51. return src, src, nil
  52. case "icecast":
  53. src := icecast.New(
  54. "icecast-main",
  55. cfg.Ingest.Icecast.URL,
  56. deps.HTTP,
  57. icecast.ReconnectConfig{
  58. Enabled: cfg.Ingest.Reconnect.Enabled,
  59. InitialBackoffMs: cfg.Ingest.Reconnect.InitialBackoffMs,
  60. MaxBackoffMs: cfg.Ingest.Reconnect.MaxBackoffMs,
  61. },
  62. icecast.WithDecoderPreference(cfg.Ingest.Icecast.Decoder),
  63. )
  64. return src, nil, nil
  65. case "srt":
  66. srtCfg := aoiprxkit.SRTConfig{
  67. URL: cfg.Ingest.SRT.URL,
  68. Mode: cfg.Ingest.SRT.Mode,
  69. SampleRateHz: cfg.Ingest.SRT.SampleRateHz,
  70. Channels: cfg.Ingest.SRT.Channels,
  71. }
  72. opts := []srt.Option{}
  73. if deps.SRTOpener != nil {
  74. opts = append(opts, srt.WithConnOpener(deps.SRTOpener))
  75. }
  76. src := srt.New("srt-main", srtCfg, opts...)
  77. return src, nil, nil
  78. case "aes67", "aoip", "aoip-rtp":
  79. aoipCfg, detail, err := buildAES67Config(cfg, deps)
  80. if err != nil {
  81. return nil, nil, err
  82. }
  83. opts := []aoip.Option{}
  84. if deps.AES67ReceiverFactory != nil {
  85. opts = append(opts, aoip.WithReceiverFactory(deps.AES67ReceiverFactory))
  86. }
  87. if detail != "" {
  88. opts = append(opts, aoip.WithDetail(detail))
  89. }
  90. src := aoip.New("aes67-main", aoipCfg, opts...)
  91. return src, nil, nil
  92. default:
  93. return nil, nil, fmt.Errorf("unsupported ingest kind: %s", cfg.Ingest.Kind)
  94. }
  95. }
  96. func SampleRateForKind(cfg config.Config) int {
  97. switch normalizeIngestKind(cfg.Ingest.Kind) {
  98. case "stdin", "stdin-pcm":
  99. if cfg.Ingest.Stdin.SampleRateHz > 0 {
  100. return cfg.Ingest.Stdin.SampleRateHz
  101. }
  102. case "http-raw":
  103. if cfg.Ingest.HTTPRaw.SampleRateHz > 0 {
  104. return cfg.Ingest.HTTPRaw.SampleRateHz
  105. }
  106. case "icecast":
  107. return 44100
  108. case "srt":
  109. if cfg.Ingest.SRT.SampleRateHz > 0 {
  110. return cfg.Ingest.SRT.SampleRateHz
  111. }
  112. case "aes67", "aoip", "aoip-rtp":
  113. if cfg.Ingest.AES67.SampleRateHz > 0 {
  114. return cfg.Ingest.AES67.SampleRateHz
  115. }
  116. }
  117. return 44100
  118. }
  119. func normalizeIngestKind(kind string) string {
  120. return strings.ToLower(strings.TrimSpace(kind))
  121. }
  122. func buildAES67Config(cfg config.Config, deps Deps) (aoiprxkit.Config, string, error) {
  123. base := aoiprxkit.DefaultConfig()
  124. ing := cfg.Ingest.AES67
  125. if strings.TrimSpace(ing.InterfaceName) != "" {
  126. base.InterfaceName = strings.TrimSpace(ing.InterfaceName)
  127. }
  128. if ing.PayloadType >= 0 {
  129. base.PayloadType = uint8(ing.PayloadType)
  130. }
  131. if ing.SampleRateHz > 0 {
  132. base.SampleRateHz = ing.SampleRateHz
  133. }
  134. if ing.Channels > 0 {
  135. base.Channels = ing.Channels
  136. }
  137. if strings.TrimSpace(ing.Encoding) != "" {
  138. base.Encoding = strings.ToUpper(strings.TrimSpace(ing.Encoding))
  139. }
  140. if ing.PacketTimeMs > 0 {
  141. base.PacketTime = time.Duration(ing.PacketTimeMs) * time.Millisecond
  142. }
  143. if ing.JitterDepthPackets > 0 {
  144. base.JitterDepthPackets = ing.JitterDepthPackets
  145. }
  146. if ing.ReadBufferBytes > 0 {
  147. base.ReadBufferBytes = ing.ReadBufferBytes
  148. }
  149. sdpText, discoveredStreamName, err := resolveAES67SDP(ing, deps)
  150. if err != nil {
  151. return aoiprxkit.Config{}, "", err
  152. }
  153. if sdpText != "" {
  154. info, err := aoiprxkit.ParseMinimalSDP(sdpText)
  155. if err != nil {
  156. return aoiprxkit.Config{}, "", fmt.Errorf("parse ingest.aes67 SDP: %w", err)
  157. }
  158. parsed, err := aoiprxkit.ConfigFromSDP(base, info)
  159. if err != nil {
  160. return aoiprxkit.Config{}, "", fmt.Errorf("map ingest.aes67 SDP: %w", err)
  161. }
  162. detail := ""
  163. if discoveredStreamName != "" {
  164. detail = fmt.Sprintf("rtp://%s:%d (SAP s=%s)", parsed.MulticastGroup, parsed.Port, discoveredStreamName)
  165. }
  166. return parsed, detail, nil
  167. }
  168. if strings.TrimSpace(ing.MulticastGroup) != "" {
  169. base.MulticastGroup = strings.TrimSpace(ing.MulticastGroup)
  170. }
  171. if ing.Port > 0 {
  172. base.Port = ing.Port
  173. }
  174. if err := base.Validate(); err != nil {
  175. return aoiprxkit.Config{}, "", err
  176. }
  177. return base, "", nil
  178. }
  179. func resolveAES67SDP(ing config.IngestAES67Config, deps Deps) (string, string, error) {
  180. sdpText := strings.TrimSpace(ing.SDP)
  181. if sdpText == "" && strings.TrimSpace(ing.SDPPath) != "" {
  182. data, err := os.ReadFile(filepath.Clean(ing.SDPPath))
  183. if err != nil {
  184. return "", "", fmt.Errorf("read ingest.aes67.sdpPath: %w", err)
  185. }
  186. sdpText = string(data)
  187. }
  188. if sdpText != "" {
  189. return sdpText, "", nil
  190. }
  191. discoveryEnabled := ing.Discovery.Enabled || strings.TrimSpace(ing.Discovery.StreamName) != ""
  192. if !discoveryEnabled {
  193. return "", "", nil
  194. }
  195. timeout := time.Duration(ing.Discovery.TimeoutMs) * time.Millisecond
  196. if timeout <= 0 {
  197. timeout = 3 * time.Second
  198. }
  199. req := AES67DiscoverRequest{
  200. StreamName: strings.TrimSpace(ing.Discovery.StreamName),
  201. Timeout: timeout,
  202. InterfaceName: strings.TrimSpace(ing.Discovery.InterfaceName),
  203. SAPGroup: strings.TrimSpace(ing.Discovery.SAPGroup),
  204. SAPPort: ing.Discovery.SAPPort,
  205. }
  206. discover := deps.AES67Discover
  207. if discover == nil {
  208. discover = discoverAES67ViaSAP
  209. }
  210. announcement, err := discover(context.Background(), req)
  211. if err != nil {
  212. return "", "", fmt.Errorf("discover ingest.aes67 stream %q via SAP: %w", req.StreamName, err)
  213. }
  214. if strings.TrimSpace(announcement.SDP) == "" {
  215. return "", "", fmt.Errorf("discover ingest.aes67 stream %q via SAP: empty SDP payload", req.StreamName)
  216. }
  217. return announcement.SDP, req.StreamName, nil
  218. }
  219. func discoverAES67ViaSAP(ctx context.Context, req AES67DiscoverRequest) (aoiprxkit.SAPAnnouncement, error) {
  220. if req.StreamName == "" {
  221. return aoiprxkit.SAPAnnouncement{}, fmt.Errorf("stream name must not be empty")
  222. }
  223. listenerCfg := aoiprxkit.DefaultSAPListenerConfig()
  224. if req.InterfaceName != "" {
  225. listenerCfg.InterfaceName = req.InterfaceName
  226. }
  227. if req.SAPGroup != "" {
  228. listenerCfg.Group = req.SAPGroup
  229. }
  230. if req.SAPPort > 0 {
  231. listenerCfg.Port = req.SAPPort
  232. }
  233. sf, err := aoiprxkit.NewStreamFinder(listenerCfg)
  234. if err != nil {
  235. return aoiprxkit.SAPAnnouncement{}, err
  236. }
  237. if err := sf.Start(ctx); err != nil {
  238. return aoiprxkit.SAPAnnouncement{}, err
  239. }
  240. defer sf.Stop()
  241. waitCtx, cancel := context.WithTimeout(ctx, req.Timeout)
  242. defer cancel()
  243. return sf.WaitForStreamName(waitCtx, req.StreamName)
  244. }