|
- package icecast
-
- import (
- "bytes"
- "context"
- "errors"
- "io"
- "testing"
-
- "github.com/jan/fm-rds-tx/internal/ingest"
- "github.com/jan/fm-rds-tx/internal/ingest/decoder"
- )
-
- type testDecoder struct {
- name string
- err error
- called int
- }
-
- func (d *testDecoder) Name() string { return d.name }
-
- func (d *testDecoder) DecodeStream(_ context.Context, _ io.Reader, _ decoder.StreamMeta, _ func(ingest.PCMChunk) error) error {
- d.called++
- return d.err
- }
-
- type consumingUnsupportedDecoder struct {
- n int
- called int
- }
-
- func (d *consumingUnsupportedDecoder) Name() string { return "native-consuming-unsupported" }
-
- func (d *consumingUnsupportedDecoder) DecodeStream(_ context.Context, r io.Reader, _ decoder.StreamMeta, _ func(ingest.PCMChunk) error) error {
- d.called++
- buf := make([]byte, d.n)
- _, _ = io.ReadFull(r, buf)
- return decoder.ErrUnsupported
- }
-
- type captureStreamDecoder struct {
- name string
- called int
- payload []byte
- }
-
- func (d *captureStreamDecoder) Name() string { return d.name }
-
- func (d *captureStreamDecoder) DecodeStream(_ context.Context, r io.Reader, _ decoder.StreamMeta, _ func(ingest.PCMChunk) error) error {
- d.called++
- data, err := io.ReadAll(r)
- if err != nil {
- return err
- }
- d.payload = data
- return nil
- }
-
- func TestDecodeWithPreferenceAutoFallsBackFromNativeUnsupported(t *testing.T) {
- native := &testDecoder{name: "native", err: decoder.ErrUnsupported}
- fallback := &testDecoder{name: "ffmpeg"}
-
- reg := decoder.NewRegistry()
- reg.Register("mp3", func() decoder.Decoder { return native })
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("auto"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
- ContentType: "audio/mpeg",
- SourceID: "ice-test",
- })
- if err != nil {
- t.Fatalf("decode: %v", err)
- }
- if native.called != 1 {
- t.Fatalf("native called %d times", native.called)
- }
- if fallback.called != 1 {
- t.Fatalf("fallback called %d times", fallback.called)
- }
- }
-
- func TestDecodeWithPreferenceNativeDoesNotFallback(t *testing.T) {
- nativeErr := errors.New("decode failed")
- native := &testDecoder{name: "native", err: nativeErr}
- fallback := &testDecoder{name: "ffmpeg"}
-
- reg := decoder.NewRegistry()
- reg.Register("mp3", func() decoder.Decoder { return native })
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("native"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
- ContentType: "audio/mpeg",
- SourceID: "ice-test",
- })
- if !errors.Is(err, nativeErr) {
- t.Fatalf("expected native error, got %v", err)
- }
- if fallback.called != 0 {
- t.Fatalf("fallback should not be called, got %d", fallback.called)
- }
- }
-
- func TestDecodeWithPreferenceFFmpegOnly(t *testing.T) {
- native := &testDecoder{name: "native"}
- fallback := &testDecoder{name: "ffmpeg"}
-
- reg := decoder.NewRegistry()
- reg.Register("mp3", func() decoder.Decoder { return native })
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("ffmpeg"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
- ContentType: "audio/mpeg",
- SourceID: "ice-test",
- })
- if err != nil {
- t.Fatalf("decode: %v", err)
- }
- if native.called != 0 {
- t.Fatalf("native should not be called in ffmpeg mode, got %d", native.called)
- }
- if fallback.called != 1 {
- t.Fatalf("fallback called %d times", fallback.called)
- }
- }
-
- func TestDecodeWithPreferenceAutoUnsupportedContentTypeFallsBack(t *testing.T) {
- fallback := &testDecoder{name: "ffmpeg"}
- reg := decoder.NewRegistry()
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("auto"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
- ContentType: "application/octet-stream",
- SourceID: "ice-test",
- })
- if err != nil {
- t.Fatalf("decode: %v", err)
- }
- if fallback.called != 1 {
- t.Fatalf("fallback called %d times", fallback.called)
- }
- }
-
- func TestDecodeWithPreferenceAutoUsesOggNativeForOggContentType(t *testing.T) {
- ogg := &testDecoder{name: "oggvorbis"}
- fallback := &testDecoder{name: "ffmpeg"}
-
- reg := decoder.NewRegistry()
- reg.Register("oggvorbis", func() decoder.Decoder { return ogg })
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("auto"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
- ContentType: "audio/ogg",
- SourceID: "ice-test",
- })
- if err != nil {
- t.Fatalf("decode: %v", err)
- }
- if ogg.called != 1 {
- t.Fatalf("ogg decoder called %d times", ogg.called)
- }
- if fallback.called != 0 {
- t.Fatalf("fallback should not be called, got %d", fallback.called)
- }
- }
-
- func TestDecodeWithPreferenceAutoUsesMP3NativeForMPEGContentType(t *testing.T) {
- mp3Native := &testDecoder{name: "mp3"}
- fallback := &testDecoder{name: "ffmpeg"}
-
- reg := decoder.NewRegistry()
- reg.Register("mp3", func() decoder.Decoder { return mp3Native })
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("auto"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
- ContentType: "audio/mpeg; charset=utf-8",
- SourceID: "ice-test",
- })
- if err != nil {
- t.Fatalf("decode: %v", err)
- }
- if mp3Native.called != 1 {
- t.Fatalf("mp3 native decoder called %d times", mp3Native.called)
- }
- if fallback.called != 0 {
- t.Fatalf("fallback should not be called, got %d", fallback.called)
- }
- }
-
- func TestDecodeWithPreferenceAutoNativeErrorDoesNotFallback(t *testing.T) {
- nativeErr := errors.New("native hard failure")
- mp3Native := &testDecoder{name: "mp3", err: nativeErr}
- fallback := &testDecoder{name: "ffmpeg"}
-
- reg := decoder.NewRegistry()
- reg.Register("mp3", func() decoder.Decoder { return mp3Native })
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("auto"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
- ContentType: "audio/mpeg",
- SourceID: "ice-test",
- })
- if !errors.Is(err, nativeErr) {
- t.Fatalf("expected native error, got %v", err)
- }
- if fallback.called != 0 {
- t.Fatalf("fallback should not be called on native hard error, got %d", fallback.called)
- }
- }
-
- func TestDecodeWithPreferenceAutoFallbackSeesFullStreamAfterNativeConsumesPrefix(t *testing.T) {
- const consumed = 4
- input := []byte("0123456789abcdef")
-
- native := &consumingUnsupportedDecoder{n: consumed}
- fallback := &captureStreamDecoder{name: "ffmpeg"}
-
- reg := decoder.NewRegistry()
- reg.Register("mp3", func() decoder.Decoder { return native })
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("auto"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(input), decoder.StreamMeta{
- ContentType: "audio/mpeg",
- SourceID: "ice-test",
- })
- if err != nil {
- t.Fatalf("decode: %v", err)
- }
- if native.called != 1 {
- t.Fatalf("native called %d times", native.called)
- }
- if fallback.called != 1 {
- t.Fatalf("fallback called %d times", fallback.called)
- }
- if !bytes.Equal(fallback.payload, input) {
- t.Fatalf("fallback payload mismatch: got %q want %q", string(fallback.payload), string(input))
- }
- }
-
- func TestDecodeWithPreferenceNativeUnsupportedContentTypeFailsWithoutFallback(t *testing.T) {
- fallback := &testDecoder{name: "ffmpeg"}
- reg := decoder.NewRegistry()
- reg.Register("ffmpeg", func() decoder.Decoder { return fallback })
-
- src := New("ice-test", "http://example", nil, ReconnectConfig{},
- WithDecoderRegistry(reg),
- WithDecoderPreference("native"),
- )
-
- err := src.decodeWithPreference(context.Background(), bytes.NewReader(nil), decoder.StreamMeta{
- ContentType: "application/octet-stream",
- SourceID: "ice-test",
- })
- if err == nil {
- t.Fatal("expected native-mode select error for unsupported content-type")
- }
- if fallback.called != 0 {
- t.Fatalf("fallback should not be called in native mode, got %d", fallback.called)
- }
- }
-
- func TestWithDecoderPreferenceFallbackAliasNormalizesToFFmpeg(t *testing.T) {
- src := New("ice-test", "http://example", nil, ReconnectConfig{}, WithDecoderPreference("fallback"))
- if got := src.Descriptor().Codec; got != "ffmpeg" {
- t.Fatalf("codec=%s want ffmpeg", got)
- }
- }
|