Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

311 satır
6.1KB

  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. // BUG-2 fix: recreate channels and reset closeOnce so Stop+Start works.
  99. // closeChannels() uses sync.Once — reset it so the new channels can be closed.
  100. s.chunks = make(chan ingest.PCMChunk, 64)
  101. s.errs = make(chan error, 8)
  102. s.closeOnce = sync.Once{}
  103. var (
  104. rx *aoiprxkit.SRTReceiver
  105. err error
  106. )
  107. if s.opener != nil {
  108. rx, err = aoiprxkit.NewSRTReceiverWithOpener(s.cfg, s.opener, s.handleFrame)
  109. } else {
  110. rx, err = aoiprxkit.NewSRTReceiver(s.cfg, s.handleFrame)
  111. }
  112. if err != nil {
  113. s.started.Store(false)
  114. s.connected.Store(false)
  115. s.state.Store("failed")
  116. s.setError(err)
  117. return err
  118. }
  119. runCtx, cancel := context.WithCancel(ctx)
  120. s.cancel = cancel
  121. s.mu.Lock()
  122. s.rx = rx
  123. s.mu.Unlock()
  124. s.lastError.Store("")
  125. s.connected.Store(false)
  126. s.state.Store("connecting")
  127. if err := rx.Start(runCtx); err != nil {
  128. s.started.Store(false)
  129. s.connected.Store(false)
  130. s.state.Store("failed")
  131. s.setError(err)
  132. return err
  133. }
  134. s.connected.Store(true)
  135. s.state.Store("running")
  136. s.wg.Add(1)
  137. go func() {
  138. defer s.wg.Done()
  139. <-runCtx.Done()
  140. _ = s.stopReceiver()
  141. s.connected.Store(false)
  142. s.closeChannels()
  143. }()
  144. return nil
  145. }
  146. func (s *Source) Stop() error {
  147. if !s.started.CompareAndSwap(true, false) {
  148. return nil
  149. }
  150. if s.cancel != nil {
  151. s.cancel()
  152. }
  153. if err := s.stopReceiver(); err != nil {
  154. s.setError(err)
  155. s.state.Store("failed")
  156. }
  157. s.wg.Wait()
  158. s.connected.Store(false)
  159. state, _ := s.state.Load().(string)
  160. if state != "failed" {
  161. s.state.Store("stopped")
  162. }
  163. return nil
  164. }
  165. func (s *Source) Chunks() <-chan ingest.PCMChunk { return s.chunks }
  166. func (s *Source) Errors() <-chan error { return s.errs }
  167. func (s *Source) Stats() ingest.SourceStats {
  168. state, _ := s.state.Load().(string)
  169. last := s.lastChunkAtUnix.Load()
  170. errStr, _ := s.lastError.Load().(string)
  171. var lastChunkAt time.Time
  172. if last > 0 {
  173. lastChunkAt = time.Unix(0, last)
  174. }
  175. return ingest.SourceStats{
  176. State: state,
  177. Connected: s.connected.Load(),
  178. LastChunkAt: lastChunkAt,
  179. ChunksIn: s.chunksIn.Load(),
  180. SamplesIn: s.samplesIn.Load(),
  181. Overflows: s.overflows.Load(),
  182. Discontinuities: s.discontinuities.Load(),
  183. TransportLoss: s.transportLoss.Load(),
  184. Reorders: s.reorders.Load(),
  185. LastError: errStr,
  186. }
  187. }
  188. func (s *Source) handleFrame(frame aoiprxkit.PCMFrame) {
  189. if !s.started.Load() {
  190. return
  191. }
  192. discontinuity := false
  193. s.seqMu.Lock()
  194. if s.lastHasVal {
  195. expected := s.lastFrame + 1
  196. if frame.SequenceNumber != expected {
  197. discontinuity = true
  198. delta := int16(frame.SequenceNumber - expected)
  199. if delta > 0 {
  200. s.transportLoss.Add(uint64(delta))
  201. } else {
  202. s.reorders.Add(1)
  203. }
  204. }
  205. }
  206. s.lastFrame = frame.SequenceNumber
  207. s.lastHasVal = true
  208. s.seqMu.Unlock()
  209. chunk := ingest.PCMChunk{
  210. Samples: append([]int32(nil), frame.Samples...),
  211. Channels: frame.Channels,
  212. SampleRateHz: frame.SampleRateHz,
  213. Sequence: s.nextSeq.Add(1) - 1,
  214. Timestamp: frame.ReceivedAt,
  215. SourceID: s.id,
  216. Discontinuity: discontinuity,
  217. }
  218. s.chunksIn.Add(1)
  219. s.samplesIn.Add(uint64(len(chunk.Samples)))
  220. s.lastChunkAtUnix.Store(time.Now().UnixNano())
  221. if discontinuity {
  222. s.discontinuities.Add(1)
  223. }
  224. select {
  225. case s.chunks <- chunk:
  226. default:
  227. s.overflows.Add(1)
  228. s.discontinuities.Add(1)
  229. s.setError(io.ErrShortBuffer)
  230. s.emitError(fmt.Errorf("srt chunk buffer overflow"))
  231. }
  232. }
  233. func (s *Source) stopReceiver() error {
  234. s.mu.Lock()
  235. rx := s.rx
  236. s.rx = nil
  237. s.mu.Unlock()
  238. if rx == nil {
  239. return nil
  240. }
  241. return rx.Stop()
  242. }
  243. func (s *Source) closeChannels() {
  244. s.closeOnce.Do(func() {
  245. close(s.chunks)
  246. close(s.errs)
  247. })
  248. }
  249. func (s *Source) setError(err error) {
  250. if err == nil {
  251. return
  252. }
  253. s.lastError.Store(err.Error())
  254. s.emitError(err)
  255. }
  256. func (s *Source) emitError(err error) {
  257. if err == nil {
  258. return
  259. }
  260. select {
  261. case s.errs <- err:
  262. default:
  263. }
  264. }
  265. func redactURL(raw string) string {
  266. trimmed := strings.TrimSpace(raw)
  267. if trimmed == "" {
  268. return ""
  269. }
  270. u, err := url.Parse(trimmed)
  271. if err != nil || u.Host == "" {
  272. return trimmed
  273. }
  274. u.User = nil
  275. u.RawQuery = ""
  276. u.Fragment = ""
  277. return u.String()
  278. }