Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
9.0KB

  1. package icecast
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "io"
  7. "testing"
  8. "github.com/jan/fm-rds-tx/internal/ingest"
  9. "github.com/jan/fm-rds-tx/internal/ingest/decoder"
  10. )
  11. type testDecoder struct {
  12. name string
  13. err error
  14. called int
  15. }
  16. func (d *testDecoder) Name() string { return d.name }
  17. func (d *testDecoder) DecodeStream(_ context.Context, _ io.Reader, _ decoder.StreamMeta, _ func(ingest.PCMChunk) error) error {
  18. d.called++
  19. return d.err
  20. }
  21. type consumingUnsupportedDecoder struct {
  22. n int
  23. called int
  24. }
  25. func (d *consumingUnsupportedDecoder) Name() string { return "native-consuming-unsupported" }
  26. func (d *consumingUnsupportedDecoder) DecodeStream(_ context.Context, r io.Reader, _ decoder.StreamMeta, _ func(ingest.PCMChunk) error) error {
  27. d.called++
  28. buf := make([]byte, d.n)
  29. _, _ = io.ReadFull(r, buf)
  30. return decoder.ErrUnsupported
  31. }
  32. type captureStreamDecoder struct {
  33. name string
  34. called int
  35. payload []byte
  36. }
  37. func (d *captureStreamDecoder) Name() string { return d.name }
  38. func (d *captureStreamDecoder) DecodeStream(_ context.Context, r io.Reader, _ decoder.StreamMeta, _ func(ingest.PCMChunk) error) error {
  39. d.called++
  40. data, err := io.ReadAll(r)
  41. if err != nil {
  42. return err
  43. }
  44. d.payload = data
  45. return nil
  46. }
  47. func TestDecodeWithPreferenceAutoFallsBackFromNativeUnsupported(t *testing.T) {
  48. native := &testDecoder{name: "native", err: decoder.ErrUnsupported}
  49. fallback := &testDecoder{name: "ffmpeg"}
  50. reg := decoder.NewRegistry()
  51. reg.Register("mp3", func() decoder.Decoder { return native })
  52. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  53. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  54. WithDecoderRegistry(reg),
  55. WithDecoderPreference("auto"),
  56. )
  57. err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
  58. ContentType: "audio/mpeg",
  59. SourceID: "ice-test",
  60. })
  61. if err != nil {
  62. t.Fatalf("decode: %v", err)
  63. }
  64. if native.called != 1 {
  65. t.Fatalf("native called %d times", native.called)
  66. }
  67. if fallback.called != 1 {
  68. t.Fatalf("fallback called %d times", fallback.called)
  69. }
  70. }
  71. func TestDecodeWithPreferenceNativeDoesNotFallback(t *testing.T) {
  72. nativeErr := errors.New("decode failed")
  73. native := &testDecoder{name: "native", err: nativeErr}
  74. fallback := &testDecoder{name: "ffmpeg"}
  75. reg := decoder.NewRegistry()
  76. reg.Register("mp3", func() decoder.Decoder { return native })
  77. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  78. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  79. WithDecoderRegistry(reg),
  80. WithDecoderPreference("native"),
  81. )
  82. err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
  83. ContentType: "audio/mpeg",
  84. SourceID: "ice-test",
  85. })
  86. if !errors.Is(err, nativeErr) {
  87. t.Fatalf("expected native error, got %v", err)
  88. }
  89. if fallback.called != 0 {
  90. t.Fatalf("fallback should not be called, got %d", fallback.called)
  91. }
  92. }
  93. func TestDecodeWithPreferenceFFmpegOnly(t *testing.T) {
  94. native := &testDecoder{name: "native"}
  95. fallback := &testDecoder{name: "ffmpeg"}
  96. reg := decoder.NewRegistry()
  97. reg.Register("mp3", func() decoder.Decoder { return native })
  98. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  99. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  100. WithDecoderRegistry(reg),
  101. WithDecoderPreference("ffmpeg"),
  102. )
  103. err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
  104. ContentType: "audio/mpeg",
  105. SourceID: "ice-test",
  106. })
  107. if err != nil {
  108. t.Fatalf("decode: %v", err)
  109. }
  110. if native.called != 0 {
  111. t.Fatalf("native should not be called in ffmpeg mode, got %d", native.called)
  112. }
  113. if fallback.called != 1 {
  114. t.Fatalf("fallback called %d times", fallback.called)
  115. }
  116. }
  117. func TestDecodeWithPreferenceAutoUnsupportedContentTypeFallsBack(t *testing.T) {
  118. fallback := &testDecoder{name: "ffmpeg"}
  119. reg := decoder.NewRegistry()
  120. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  121. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  122. WithDecoderRegistry(reg),
  123. WithDecoderPreference("auto"),
  124. )
  125. err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
  126. ContentType: "application/octet-stream",
  127. SourceID: "ice-test",
  128. })
  129. if err != nil {
  130. t.Fatalf("decode: %v", err)
  131. }
  132. if fallback.called != 1 {
  133. t.Fatalf("fallback called %d times", fallback.called)
  134. }
  135. }
  136. func TestDecodeWithPreferenceAutoUsesOggNativeForOggContentType(t *testing.T) {
  137. ogg := &testDecoder{name: "oggvorbis"}
  138. fallback := &testDecoder{name: "ffmpeg"}
  139. reg := decoder.NewRegistry()
  140. reg.Register("oggvorbis", func() decoder.Decoder { return ogg })
  141. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  142. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  143. WithDecoderRegistry(reg),
  144. WithDecoderPreference("auto"),
  145. )
  146. err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
  147. ContentType: "audio/ogg",
  148. SourceID: "ice-test",
  149. })
  150. if err != nil {
  151. t.Fatalf("decode: %v", err)
  152. }
  153. if ogg.called != 1 {
  154. t.Fatalf("ogg decoder called %d times", ogg.called)
  155. }
  156. if fallback.called != 0 {
  157. t.Fatalf("fallback should not be called, got %d", fallback.called)
  158. }
  159. }
  160. func TestDecodeWithPreferenceAutoUsesMP3NativeForMPEGContentType(t *testing.T) {
  161. mp3Native := &testDecoder{name: "mp3"}
  162. fallback := &testDecoder{name: "ffmpeg"}
  163. reg := decoder.NewRegistry()
  164. reg.Register("mp3", func() decoder.Decoder { return mp3Native })
  165. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  166. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  167. WithDecoderRegistry(reg),
  168. WithDecoderPreference("auto"),
  169. )
  170. err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
  171. ContentType: "audio/mpeg; charset=utf-8",
  172. SourceID: "ice-test",
  173. })
  174. if err != nil {
  175. t.Fatalf("decode: %v", err)
  176. }
  177. if mp3Native.called != 1 {
  178. t.Fatalf("mp3 native decoder called %d times", mp3Native.called)
  179. }
  180. if fallback.called != 0 {
  181. t.Fatalf("fallback should not be called, got %d", fallback.called)
  182. }
  183. }
  184. func TestDecodeWithPreferenceAutoNativeErrorDoesNotFallback(t *testing.T) {
  185. nativeErr := errors.New("native hard failure")
  186. mp3Native := &testDecoder{name: "mp3", err: nativeErr}
  187. fallback := &testDecoder{name: "ffmpeg"}
  188. reg := decoder.NewRegistry()
  189. reg.Register("mp3", func() decoder.Decoder { return mp3Native })
  190. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  191. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  192. WithDecoderRegistry(reg),
  193. WithDecoderPreference("auto"),
  194. )
  195. err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
  196. ContentType: "audio/mpeg",
  197. SourceID: "ice-test",
  198. })
  199. if !errors.Is(err, nativeErr) {
  200. t.Fatalf("expected native error, got %v", err)
  201. }
  202. if fallback.called != 0 {
  203. t.Fatalf("fallback should not be called on native hard error, got %d", fallback.called)
  204. }
  205. }
  206. func TestDecodeWithPreferenceAutoFallbackSeesFullStreamAfterNativeConsumesPrefix(t *testing.T) {
  207. const consumed = 4
  208. input := []byte("0123456789abcdef")
  209. native := &consumingUnsupportedDecoder{n: consumed}
  210. fallback := &captureStreamDecoder{name: "ffmpeg"}
  211. reg := decoder.NewRegistry()
  212. reg.Register("mp3", func() decoder.Decoder { return native })
  213. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  214. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  215. WithDecoderRegistry(reg),
  216. WithDecoderPreference("auto"),
  217. )
  218. err := src.decodeWithPreference(context.Background(), bytes.NewReader(input), decoder.StreamMeta{
  219. ContentType: "audio/mpeg",
  220. SourceID: "ice-test",
  221. })
  222. if err != nil {
  223. t.Fatalf("decode: %v", err)
  224. }
  225. if native.called != 1 {
  226. t.Fatalf("native called %d times", native.called)
  227. }
  228. if fallback.called != 1 {
  229. t.Fatalf("fallback called %d times", fallback.called)
  230. }
  231. if !bytes.Equal(fallback.payload, input) {
  232. t.Fatalf("fallback payload mismatch: got %q want %q", string(fallback.payload), string(input))
  233. }
  234. }
  235. func TestDecodeWithPreferenceNativeUnsupportedContentTypeFailsWithoutFallback(t *testing.T) {
  236. fallback := &testDecoder{name: "ffmpeg"}
  237. reg := decoder.NewRegistry()
  238. reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
  239. src := New("ice-test", "http://example", nil, ReconnectConfig{},
  240. WithDecoderRegistry(reg),
  241. WithDecoderPreference("native"),
  242. )
  243. err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
  244. ContentType: "application/octet-stream",
  245. SourceID: "ice-test",
  246. })
  247. if err == nil {
  248. t.Fatal("expected native-mode select error for unsupported content-type")
  249. }
  250. if fallback.called != 0 {
  251. t.Fatalf("fallback should not be called in native mode, got %d", fallback.called)
  252. }
  253. }
  254. func TestWithDecoderPreferenceFallbackAliasNormalizesToFFmpeg(t *testing.T) {
  255. src := New("ice-test", "http://example", nil, ReconnectConfig{}, WithDecoderPreference("fallback"))
  256. if got := src.Descriptor().Codec; got != "ffmpeg" {
  257. t.Fatalf("codec=%s want ffmpeg", got)
  258. }
  259. }