Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

167 lignes
4.1KB

  1. package fallback
  2. import (
  3. "context"
  4. "encoding/binary"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "os/exec"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/jan/fm-rds-tx/internal/ingest"
  13. "github.com/jan/fm-rds-tx/internal/ingest/decoder"
  14. )
  15. type FFmpegDecoder struct{}
  16. func NewFFmpeg() *FFmpegDecoder { return &FFmpegDecoder{} }
  17. func (d *FFmpegDecoder) Name() string { return "ffmpeg-fallback" }
  18. func (d *FFmpegDecoder) DecodeStream(ctx context.Context, r io.Reader, meta decoder.StreamMeta, emit func(ingest.PCMChunk) error) error {
  19. if r == nil {
  20. return fmt.Errorf("%w: ffmpeg decoder stream reader is nil", decoder.ErrUnsupported)
  21. }
  22. if emit == nil {
  23. return fmt.Errorf("%w: ffmpeg decoder emit callback is nil", decoder.ErrUnsupported)
  24. }
  25. sampleRate := meta.SampleRateHz
  26. if sampleRate <= 0 {
  27. sampleRate = 44100
  28. }
  29. channels := meta.Channels
  30. if channels <= 0 {
  31. channels = 2
  32. }
  33. cmd := exec.CommandContext(ctx,
  34. "ffmpeg",
  35. "-hide_banner", "-loglevel", "error",
  36. "-i", "pipe:0",
  37. "-f", "s16le",
  38. "-acodec", "pcm_s16le",
  39. "-ac", fmt.Sprintf("%d", channels),
  40. "-ar", fmt.Sprintf("%d", sampleRate),
  41. "pipe:1",
  42. )
  43. stdin, err := cmd.StdinPipe()
  44. if err != nil {
  45. return fmt.Errorf("ffmpeg stdin pipe: %w", err)
  46. }
  47. stdout, err := cmd.StdoutPipe()
  48. if err != nil {
  49. return fmt.Errorf("ffmpeg stdout pipe: %w", err)
  50. }
  51. stderr, err := cmd.StderrPipe()
  52. if err != nil {
  53. return fmt.Errorf("ffmpeg stderr pipe: %w", err)
  54. }
  55. if err := cmd.Start(); err != nil {
  56. if errorsIsNotFound(err) {
  57. return fmt.Errorf("%w: ffmpeg executable not found in PATH", decoder.ErrUnsupported)
  58. }
  59. return fmt.Errorf("ffmpeg start: %w", err)
  60. }
  61. errCh := make(chan error, 2)
  62. var wg sync.WaitGroup
  63. wg.Add(1)
  64. go func() {
  65. defer wg.Done()
  66. _, copyErr := io.Copy(stdin, r)
  67. _ = stdin.Close()
  68. if copyErr != nil && ctx.Err() == nil {
  69. errCh <- fmt.Errorf("ffmpeg stdin copy: %w", copyErr)
  70. }
  71. }()
  72. // DEADLOCK FIX: stderr and stdout must be drained concurrently.
  73. // Reading stderr synchronously before readPCM means ffmpeg blocks when
  74. // stdout's pipe buffer fills (typically 64KB), which prevents it from
  75. // closing stderr, which prevents ReadAll from returning — deadlock.
  76. var stderrData []byte
  77. wg.Add(1)
  78. go func() {
  79. defer wg.Done()
  80. stderrData, _ = io.ReadAll(stderr)
  81. }()
  82. readErr := d.readPCM(ctx, stdout, sampleRate, channels, meta.SourceID, emit)
  83. waitErr := cmd.Wait()
  84. wg.Wait()
  85. close(errCh)
  86. for e := range errCh {
  87. if e != nil {
  88. return e
  89. }
  90. }
  91. if readErr != nil {
  92. return readErr
  93. }
  94. if waitErr != nil && ctx.Err() == nil {
  95. msg := strings.TrimSpace(string(stderrData))
  96. if msg != "" {
  97. return fmt.Errorf("ffmpeg decode: %w (%s)", waitErr, msg)
  98. }
  99. return fmt.Errorf("ffmpeg decode: %w", waitErr)
  100. }
  101. return nil
  102. }
  103. func (d *FFmpegDecoder) readPCM(ctx context.Context, r io.Reader, sampleRate, channels int, sourceID string, emit func(ingest.PCMChunk) error) error {
  104. const chunkFrames = 1024
  105. frameBytes := channels * 2
  106. buf := make([]byte, chunkFrames*frameBytes)
  107. seq := uint64(0)
  108. for {
  109. select {
  110. case <-ctx.Done():
  111. return nil
  112. default:
  113. }
  114. n, err := io.ReadAtLeast(r, buf, frameBytes)
  115. if err != nil {
  116. if err == io.EOF || err == io.ErrUnexpectedEOF {
  117. if n > 0 {
  118. if emitErr := emitPCM(buf[:n], seq, sampleRate, channels, sourceID, emit); emitErr != nil {
  119. return emitErr
  120. }
  121. }
  122. return nil
  123. }
  124. return fmt.Errorf("ffmpeg read pcm: %w", err)
  125. }
  126. if emitErr := emitPCM(buf[:n], seq, sampleRate, channels, sourceID, emit); emitErr != nil {
  127. return emitErr
  128. }
  129. seq++
  130. }
  131. }
  132. func emitPCM(data []byte, seq uint64, sampleRate, channels int, sourceID string, emit func(ingest.PCMChunk) error) error {
  133. samples := make([]int32, 0, len(data)/2)
  134. for i := 0; i+1 < len(data); i += 2 {
  135. v := int16(binary.LittleEndian.Uint16(data[i : i+2]))
  136. samples = append(samples, int32(v)<<16)
  137. }
  138. return emit(ingest.PCMChunk{
  139. Samples: samples,
  140. Channels: channels,
  141. SampleRateHz: sampleRate,
  142. Sequence: seq,
  143. Timestamp: time.Now(),
  144. SourceID: sourceID,
  145. })
  146. }
  147. func errorsIsNotFound(err error) bool {
  148. var execErr *exec.Error
  149. return err != nil && (errors.As(err, &execErr) || strings.Contains(strings.ToLower(err.Error()), "executable file not found"))
  150. }