Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

306 líneas
5.8KB

  1. package srt
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net/url"
  7. "strings"
  8. "sync"
  9. "sync/atomic"
  10. "time"
  11. "aoiprxkit"
  12. "github.com/jan/fm-rds-tx/internal/ingest"
  13. )
  14. type Option func(*Source)
  15. func WithConnOpener(opener aoiprxkit.SRTConnOpener) Option {
  16. return func(s *Source) {
  17. if opener != nil {
  18. s.opener = opener
  19. }
  20. }
  21. }
  22. type Source struct {
  23. id string
  24. cfg aoiprxkit.SRTConfig
  25. opener aoiprxkit.SRTConnOpener
  26. chunks chan ingest.PCMChunk
  27. errs chan error
  28. cancel context.CancelFunc
  29. wg sync.WaitGroup
  30. mu sync.Mutex
  31. rx *aoiprxkit.SRTReceiver
  32. started atomic.Bool
  33. closeOnce sync.Once
  34. state atomic.Value // string
  35. connected atomic.Bool
  36. chunksIn atomic.Uint64
  37. samplesIn atomic.Uint64
  38. overflows atomic.Uint64
  39. discontinuities atomic.Uint64
  40. transportLoss atomic.Uint64
  41. reorders atomic.Uint64
  42. lastChunkAtUnix atomic.Int64
  43. lastError atomic.Value // string
  44. nextSeq atomic.Uint64
  45. seqMu sync.Mutex
  46. lastFrame uint16
  47. lastHasVal bool
  48. }
  49. func New(id string, cfg aoiprxkit.SRTConfig, opts ...Option) *Source {
  50. if id == "" {
  51. id = "srt-main"
  52. }
  53. if cfg.Mode == "" {
  54. cfg.Mode = "listener"
  55. }
  56. if cfg.SampleRateHz <= 0 {
  57. cfg.SampleRateHz = 48000
  58. }
  59. if cfg.Channels <= 0 {
  60. cfg.Channels = 2
  61. }
  62. s := &Source{
  63. id: id,
  64. cfg: cfg,
  65. chunks: make(chan ingest.PCMChunk, 64),
  66. errs: make(chan error, 8),
  67. }
  68. for _, opt := range opts {
  69. if opt != nil {
  70. opt(s)
  71. }
  72. }
  73. s.state.Store("idle")
  74. s.lastError.Store("")
  75. return s
  76. }
  77. func (s *Source) Descriptor() ingest.SourceDescriptor {
  78. return ingest.SourceDescriptor{
  79. ID: s.id,
  80. Kind: "srt",
  81. Family: "aoip",
  82. Transport: "srt",
  83. Codec: "pcm_s32le",
  84. Channels: s.cfg.Channels,
  85. SampleRateHz: s.cfg.SampleRateHz,
  86. Detail: s.cfg.URL,
  87. Origin: &ingest.SourceOrigin{
  88. Kind: "url",
  89. Endpoint: redactURL(s.cfg.URL),
  90. Mode: strings.TrimSpace(s.cfg.Mode),
  91. },
  92. }
  93. }
  94. func (s *Source) Start(ctx context.Context) error {
  95. if !s.started.CompareAndSwap(false, true) {
  96. return nil
  97. }
  98. var (
  99. rx *aoiprxkit.SRTReceiver
  100. err error
  101. )
  102. if s.opener != nil {
  103. rx, err = aoiprxkit.NewSRTReceiverWithOpener(s.cfg, s.opener, s.handleFrame)
  104. } else {
  105. rx, err = aoiprxkit.NewSRTReceiver(s.cfg, s.handleFrame)
  106. }
  107. if err != nil {
  108. s.started.Store(false)
  109. s.connected.Store(false)
  110. s.state.Store("failed")
  111. s.setError(err)
  112. return err
  113. }
  114. runCtx, cancel := context.WithCancel(ctx)
  115. s.cancel = cancel
  116. s.mu.Lock()
  117. s.rx = rx
  118. s.mu.Unlock()
  119. s.lastError.Store("")
  120. s.connected.Store(false)
  121. s.state.Store("connecting")
  122. if err := rx.Start(runCtx); err != nil {
  123. s.started.Store(false)
  124. s.connected.Store(false)
  125. s.state.Store("failed")
  126. s.setError(err)
  127. return err
  128. }
  129. s.connected.Store(true)
  130. s.state.Store("running")
  131. s.wg.Add(1)
  132. go func() {
  133. defer s.wg.Done()
  134. <-runCtx.Done()
  135. _ = s.stopReceiver()
  136. s.connected.Store(false)
  137. s.closeChannels()
  138. }()
  139. return nil
  140. }
  141. func (s *Source) Stop() error {
  142. if !s.started.CompareAndSwap(true, false) {
  143. return nil
  144. }
  145. if s.cancel != nil {
  146. s.cancel()
  147. }
  148. if err := s.stopReceiver(); err != nil {
  149. s.setError(err)
  150. s.state.Store("failed")
  151. }
  152. s.wg.Wait()
  153. s.connected.Store(false)
  154. state, _ := s.state.Load().(string)
  155. if state != "failed" {
  156. s.state.Store("stopped")
  157. }
  158. return nil
  159. }
  160. func (s *Source) Chunks() <-chan ingest.PCMChunk { return s.chunks }
  161. func (s *Source) Errors() <-chan error { return s.errs }
  162. func (s *Source) Stats() ingest.SourceStats {
  163. state, _ := s.state.Load().(string)
  164. last := s.lastChunkAtUnix.Load()
  165. errStr, _ := s.lastError.Load().(string)
  166. var lastChunkAt time.Time
  167. if last > 0 {
  168. lastChunkAt = time.Unix(0, last)
  169. }
  170. return ingest.SourceStats{
  171. State: state,
  172. Connected: s.connected.Load(),
  173. LastChunkAt: lastChunkAt,
  174. ChunksIn: s.chunksIn.Load(),
  175. SamplesIn: s.samplesIn.Load(),
  176. Overflows: s.overflows.Load(),
  177. Discontinuities: s.discontinuities.Load(),
  178. TransportLoss: s.transportLoss.Load(),
  179. Reorders: s.reorders.Load(),
  180. LastError: errStr,
  181. }
  182. }
  183. func (s *Source) handleFrame(frame aoiprxkit.PCMFrame) {
  184. if !s.started.Load() {
  185. return
  186. }
  187. discontinuity := false
  188. s.seqMu.Lock()
  189. if s.lastHasVal {
  190. expected := s.lastFrame + 1
  191. if frame.SequenceNumber != expected {
  192. discontinuity = true
  193. delta := int16(frame.SequenceNumber - expected)
  194. if delta > 0 {
  195. s.transportLoss.Add(uint64(delta))
  196. } else {
  197. s.reorders.Add(1)
  198. }
  199. }
  200. }
  201. s.lastFrame = frame.SequenceNumber
  202. s.lastHasVal = true
  203. s.seqMu.Unlock()
  204. chunk := ingest.PCMChunk{
  205. Samples: append([]int32(nil), frame.Samples...),
  206. Channels: frame.Channels,
  207. SampleRateHz: frame.SampleRateHz,
  208. Sequence: s.nextSeq.Add(1) - 1,
  209. Timestamp: frame.ReceivedAt,
  210. SourceID: s.id,
  211. Discontinuity: discontinuity,
  212. }
  213. s.chunksIn.Add(1)
  214. s.samplesIn.Add(uint64(len(chunk.Samples)))
  215. s.lastChunkAtUnix.Store(time.Now().UnixNano())
  216. if discontinuity {
  217. s.discontinuities.Add(1)
  218. }
  219. select {
  220. case s.chunks <- chunk:
  221. default:
  222. s.overflows.Add(1)
  223. s.discontinuities.Add(1)
  224. s.setError(io.ErrShortBuffer)
  225. s.emitError(fmt.Errorf("srt chunk buffer overflow"))
  226. }
  227. }
  228. func (s *Source) stopReceiver() error {
  229. s.mu.Lock()
  230. rx := s.rx
  231. s.rx = nil
  232. s.mu.Unlock()
  233. if rx == nil {
  234. return nil
  235. }
  236. return rx.Stop()
  237. }
  238. func (s *Source) closeChannels() {
  239. s.closeOnce.Do(func() {
  240. close(s.chunks)
  241. close(s.errs)
  242. })
  243. }
  244. func (s *Source) setError(err error) {
  245. if err == nil {
  246. return
  247. }
  248. s.lastError.Store(err.Error())
  249. s.emitError(err)
  250. }
  251. func (s *Source) emitError(err error) {
  252. if err == nil {
  253. return
  254. }
  255. select {
  256. case s.errs <- err:
  257. default:
  258. }
  259. }
  260. func redactURL(raw string) string {
  261. trimmed := strings.TrimSpace(raw)
  262. if trimmed == "" {
  263. return ""
  264. }
  265. u, err := url.Parse(trimmed)
  266. if err != nil || u.Host == "" {
  267. return trimmed
  268. }
  269. u.User = nil
  270. u.RawQuery = ""
  271. u.Fragment = ""
  272. return u.String()
  273. }