Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

158 linhas
3.8KB

  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. stderrData, _ := io.ReadAll(stderr)
  73. readErr := d.readPCM(ctx, stdout, sampleRate, channels, meta.SourceID, emit)
  74. waitErr := cmd.Wait()
  75. wg.Wait()
  76. close(errCh)
  77. for e := range errCh {
  78. if e != nil {
  79. return e
  80. }
  81. }
  82. if readErr != nil {
  83. return readErr
  84. }
  85. if waitErr != nil && ctx.Err() == nil {
  86. msg := strings.TrimSpace(string(stderrData))
  87. if msg != "" {
  88. return fmt.Errorf("ffmpeg decode: %w (%s)", waitErr, msg)
  89. }
  90. return fmt.Errorf("ffmpeg decode: %w", waitErr)
  91. }
  92. return nil
  93. }
  94. func (d *FFmpegDecoder) readPCM(ctx context.Context, r io.Reader, sampleRate, channels int, sourceID string, emit func(ingest.PCMChunk) error) error {
  95. const chunkFrames = 1024
  96. frameBytes := channels * 2
  97. buf := make([]byte, chunkFrames*frameBytes)
  98. seq := uint64(0)
  99. for {
  100. select {
  101. case <-ctx.Done():
  102. return nil
  103. default:
  104. }
  105. n, err := io.ReadAtLeast(r, buf, frameBytes)
  106. if err != nil {
  107. if err == io.EOF || err == io.ErrUnexpectedEOF {
  108. if n > 0 {
  109. if emitErr := emitPCM(buf[:n], seq, sampleRate, channels, sourceID, emit); emitErr != nil {
  110. return emitErr
  111. }
  112. }
  113. return nil
  114. }
  115. return fmt.Errorf("ffmpeg read pcm: %w", err)
  116. }
  117. if emitErr := emitPCM(buf[:n], seq, sampleRate, channels, sourceID, emit); emitErr != nil {
  118. return emitErr
  119. }
  120. seq++
  121. }
  122. }
  123. func emitPCM(data []byte, seq uint64, sampleRate, channels int, sourceID string, emit func(ingest.PCMChunk) error) error {
  124. samples := make([]int32, 0, len(data)/2)
  125. for i := 0; i+1 < len(data); i += 2 {
  126. v := int16(binary.LittleEndian.Uint16(data[i : i+2]))
  127. samples = append(samples, int32(v)<<16)
  128. }
  129. return emit(ingest.PCMChunk{
  130. Samples: samples,
  131. Channels: channels,
  132. SampleRateHz: sampleRate,
  133. Sequence: seq,
  134. Timestamp: time.Now(),
  135. SourceID: sourceID,
  136. })
  137. }
  138. func errorsIsNotFound(err error) bool {
  139. var execErr *exec.Error
  140. return err != nil && (errors.As(err, &execErr) || strings.Contains(strings.ToLower(err.Error()), "executable file not found"))
  141. }