package factory import ( "bytes" "context" "io" "testing" "time" "aoiprxkit" "github.com/jan/fm-rds-tx/internal/audio" "github.com/jan/fm-rds-tx/internal/config" "github.com/jan/fm-rds-tx/internal/ingest" aoipad "github.com/jan/fm-rds-tx/internal/ingest/adapters/aoip" ) type streamReadCloser struct{ io.Reader } func (r streamReadCloser) Close() error { return nil } type stubAES67Receiver struct { onStart func() } func (r *stubAES67Receiver) Start(context.Context) error { if r.onStart != nil { r.onStart() } return nil } func (r *stubAES67Receiver) Stop() error { return nil } func (r *stubAES67Receiver) Stats() aoiprxkit.Stats { return aoiprxkit.Stats{} } func TestHTTPRawFactoryToRuntimeSmoke(t *testing.T) { cfg := config.Default() cfg.Ingest.Kind = "http-raw" cfg.Ingest.HTTPRaw.SampleRateHz = 44100 cfg.Ingest.HTTPRaw.Channels = 2 src, ingress, err := BuildSource(cfg, Deps{}) if err != nil { t.Fatalf("build source: %v", err) } if src == nil || ingress == nil { t.Fatalf("expected source and ingress for kind=http-raw") } sink := audio.NewStreamSource(128, cfg.Ingest.HTTPRaw.SampleRateHz) rt := ingest.NewRuntime(sink, src) if err := rt.Start(context.Background()); err != nil { t.Fatalf("runtime start: %v", err) } defer rt.Stop() // Two stereo frames: L1,R1,L2,R2 (S16LE). frames, err := ingress.WritePCM16([]byte{ 0xE8, 0x03, 0x18, 0xFC, 0xD0, 0x07, 0x30, 0xF8, }) if err != nil { t.Fatalf("write pcm16: %v", err) } if frames != 2 { t.Fatalf("frames=%d want 2", frames) } waitForSinkFrames(t, sink, 2) stats := rt.Stats() if stats.Active.Kind != "http-raw" { t.Fatalf("active kind=%q want http-raw", stats.Active.Kind) } if stats.Source.ChunksIn != 1 { t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn) } if stats.Source.SamplesIn != 4 { t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn) } if stats.Runtime.State != "running" { t.Fatalf("runtime state=%q want running", stats.Runtime.State) } if stats.Runtime.LastChunkAt.IsZero() { t.Fatalf("runtime lastChunkAt should be set") } } func TestSRTFactoryToRuntimeSmoke(t *testing.T) { var stream bytes.Buffer if err := aoiprxkit.WritePCM32Packet(&stream, 2, 48000, 2, 1, 480, []int32{11, -11, 22, -22}); err != nil { t.Fatalf("write packet: %v", err) } cfg := config.Default() cfg.Ingest.Kind = "srt" cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000?mode=listener" cfg.Ingest.SRT.SampleRateHz = 48000 cfg.Ingest.SRT.Channels = 2 src, ingress, err := BuildSource(cfg, Deps{ SRTOpener: func(ctx context.Context, srtCfg aoiprxkit.SRTConfig) (io.ReadCloser, error) { _ = ctx _ = srtCfg return streamReadCloser{Reader: bytes.NewReader(stream.Bytes())}, nil }, }) if err != nil { t.Fatalf("build source: %v", err) } if src == nil { t.Fatalf("expected source for kind=srt") } if ingress != nil { t.Fatalf("expected no ingress for kind=srt") } sink := audio.NewStreamSource(128, cfg.Ingest.SRT.SampleRateHz) rt := ingest.NewRuntime(sink, src) if err := rt.Start(context.Background()); err != nil { t.Fatalf("runtime start: %v", err) } defer rt.Stop() waitForSinkFrames(t, sink, 2) stats := rt.Stats() if stats.Active.Kind != "srt" { t.Fatalf("active kind=%q want srt", stats.Active.Kind) } if stats.Source.ChunksIn != 1 { t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn) } if stats.Source.SamplesIn != 4 { t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn) } } func TestAES67FactoryToRuntimeSmoke(t *testing.T) { cfg := config.Default() cfg.Ingest.Kind = "aes67" cfg.Ingest.AES67.MulticastGroup = "239.10.20.30" cfg.Ingest.AES67.Port = 5004 cfg.Ingest.AES67.SampleRateHz = 48000 cfg.Ingest.AES67.Channels = 2 cfg.Ingest.AES67.Encoding = "L24" cfg.Ingest.AES67.PacketTimeMs = 1 var frameHandler aoiprxkit.FrameHandler src, ingress, err := BuildSource(cfg, Deps{ AES67ReceiverFactory: func(_ aoiprxkit.Config, onFrame aoiprxkit.FrameHandler) (aoipad.ReceiverClient, error) { frameHandler = onFrame return &stubAES67Receiver{ onStart: func() { frameHandler(aoiprxkit.PCMFrame{ SequenceNumber: 1, SampleRateHz: 48000, Channels: 2, Samples: []int32{7, -7, 9, -9}, ReceivedAt: time.Now(), }) }, }, nil }, }) if err != nil { t.Fatalf("build source: %v", err) } if src == nil { t.Fatalf("expected source for kind=aes67") } if ingress != nil { t.Fatalf("expected no ingress for kind=aes67") } sink := audio.NewStreamSource(128, cfg.Ingest.AES67.SampleRateHz) rt := ingest.NewRuntime(sink, src) if err := rt.Start(context.Background()); err != nil { t.Fatalf("runtime start: %v", err) } defer rt.Stop() waitForSinkFrames(t, sink, 2) stats := rt.Stats() if stats.Active.Kind != "aes67" { t.Fatalf("active kind=%q want aes67", stats.Active.Kind) } if stats.Source.ChunksIn != 1 { t.Fatalf("source chunksIn=%d want 1", stats.Source.ChunksIn) } if stats.Source.SamplesIn != 4 { t.Fatalf("source samplesIn=%d want 4", stats.Source.SamplesIn) } } func waitForSinkFrames(t *testing.T, sink *audio.StreamSource, minFrames int) { t.Helper() deadline := time.Now().Add(1 * time.Second) for time.Now().Before(deadline) { if sink.Available() >= minFrames { return } time.Sleep(10 * time.Millisecond) } t.Fatalf("timeout waiting for sink frames: have=%d want>=%d", sink.Available(), minFrames) }