Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

187 строки
3.1KB

  1. package ingest
  2. import (
  3. "context"
  4. "sync"
  5. "sync/atomic"
  6. "time"
  7. "github.com/jan/fm-rds-tx/internal/audio"
  8. )
  9. type Runtime struct {
  10. sink *audio.StreamSource
  11. source Source
  12. started atomic.Bool
  13. onTitle func(string)
  14. ctx context.Context
  15. cancel context.CancelFunc
  16. wg sync.WaitGroup
  17. mu sync.RWMutex
  18. active SourceDescriptor
  19. stats RuntimeStats
  20. }
  21. type RuntimeOption func(*Runtime)
  22. func WithStreamTitleHandler(handler func(string)) RuntimeOption {
  23. return func(r *Runtime) {
  24. r.onTitle = handler
  25. }
  26. }
  27. func NewRuntime(sink *audio.StreamSource, src Source, opts ...RuntimeOption) *Runtime {
  28. r := &Runtime{
  29. sink: sink,
  30. source: src,
  31. stats: RuntimeStats{
  32. State: "idle",
  33. },
  34. }
  35. for _, opt := range opts {
  36. if opt != nil {
  37. opt(r)
  38. }
  39. }
  40. return r
  41. }
  42. func (r *Runtime) Start(ctx context.Context) error {
  43. if r.sink == nil {
  44. r.mu.Lock()
  45. r.stats.State = "failed"
  46. r.mu.Unlock()
  47. return nil
  48. }
  49. if r.source == nil {
  50. r.mu.Lock()
  51. r.stats.State = "idle"
  52. r.mu.Unlock()
  53. return nil
  54. }
  55. if !r.started.CompareAndSwap(false, true) {
  56. return nil
  57. }
  58. r.ctx, r.cancel = context.WithCancel(ctx)
  59. r.mu.Lock()
  60. r.active = r.source.Descriptor()
  61. r.stats.State = "starting"
  62. r.mu.Unlock()
  63. if err := r.source.Start(r.ctx); err != nil {
  64. r.started.Store(false)
  65. r.mu.Lock()
  66. r.stats.State = "failed"
  67. r.mu.Unlock()
  68. return err
  69. }
  70. r.wg.Add(1)
  71. go r.run()
  72. return nil
  73. }
  74. func (r *Runtime) Stop() error {
  75. if !r.started.CompareAndSwap(true, false) {
  76. return nil
  77. }
  78. if r.cancel != nil {
  79. r.cancel()
  80. }
  81. if r.source != nil {
  82. _ = r.source.Stop()
  83. }
  84. r.wg.Wait()
  85. r.mu.Lock()
  86. r.stats.State = "stopped"
  87. r.mu.Unlock()
  88. return nil
  89. }
  90. func (r *Runtime) run() {
  91. defer r.wg.Done()
  92. r.mu.Lock()
  93. r.stats.State = "running"
  94. r.mu.Unlock()
  95. ch := r.source.Chunks()
  96. errCh := r.source.Errors()
  97. var titleCh <-chan string
  98. if src, ok := r.source.(StreamTitleSource); ok && r.onTitle != nil {
  99. titleCh = src.StreamTitleUpdates()
  100. }
  101. for {
  102. select {
  103. case <-r.ctx.Done():
  104. return
  105. case err, ok := <-errCh:
  106. if !ok {
  107. errCh = nil
  108. continue
  109. }
  110. if err == nil {
  111. continue
  112. }
  113. r.mu.Lock()
  114. r.stats.State = "degraded"
  115. r.mu.Unlock()
  116. case chunk, ok := <-ch:
  117. if !ok {
  118. r.mu.Lock()
  119. r.stats.State = "stopped"
  120. r.mu.Unlock()
  121. return
  122. }
  123. r.handleChunk(chunk)
  124. case title, ok := <-titleCh:
  125. if !ok {
  126. titleCh = nil
  127. continue
  128. }
  129. r.onTitle(title)
  130. }
  131. }
  132. }
  133. func (r *Runtime) handleChunk(chunk PCMChunk) {
  134. frames, err := ChunkToFrames(chunk)
  135. if err != nil {
  136. r.mu.Lock()
  137. r.stats.ConvertErrors++
  138. r.stats.State = "degraded"
  139. r.mu.Unlock()
  140. return
  141. }
  142. dropped := uint64(0)
  143. for _, frame := range frames {
  144. if !r.sink.WriteFrame(frame) {
  145. dropped++
  146. }
  147. }
  148. r.mu.Lock()
  149. r.stats.State = "running"
  150. r.stats.LastChunkAt = time.Now()
  151. r.stats.DroppedFrames += dropped
  152. r.stats.WriteBlocked = dropped > 0
  153. r.mu.Unlock()
  154. }
  155. func (r *Runtime) Stats() Stats {
  156. r.mu.RLock()
  157. runtimeStats := r.stats
  158. active := r.active
  159. r.mu.RUnlock()
  160. sourceStats := SourceStats{}
  161. if r.source != nil {
  162. sourceStats = r.source.Stats()
  163. }
  164. return Stats{
  165. Active: active,
  166. Source: sourceStats,
  167. Runtime: runtimeStats,
  168. }
  169. }