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) } }