| @@ -221,7 +221,14 @@ func TestRuntimeIncludesDetailedIngestSourceAndRuntimeStats(t *testing.T) { | |||||
| srv := NewServer(cfgpkg.Default()) | srv := NewServer(cfgpkg.Default()) | ||||
| srv.SetIngestRuntime(&fakeIngestRuntime{ | srv.SetIngestRuntime(&fakeIngestRuntime{ | ||||
| stats: ingest.Stats{ | stats: ingest.Stats{ | ||||
| Active: ingest.SourceDescriptor{ID: "icecast-main", Kind: "icecast"}, | |||||
| Active: ingest.SourceDescriptor{ | |||||
| ID: "icecast-main", | |||||
| Kind: "icecast", | |||||
| Origin: &ingest.SourceOrigin{ | |||||
| Kind: "url", | |||||
| Endpoint: "http://example.org/live", | |||||
| }, | |||||
| }, | |||||
| Source: ingest.SourceStats{ | Source: ingest.SourceStats{ | ||||
| State: "reconnecting", | State: "reconnecting", | ||||
| Connected: false, | Connected: false, | ||||
| @@ -261,6 +268,20 @@ func TestRuntimeIncludesDetailedIngestSourceAndRuntimeStats(t *testing.T) { | |||||
| if source["lastError"] != "dial tcp timeout" { | if source["lastError"] != "dial tcp timeout" { | ||||
| t.Fatalf("source lastError mismatch: got %v", source["lastError"]) | t.Fatalf("source lastError mismatch: got %v", source["lastError"]) | ||||
| } | } | ||||
| active, ok := ingestPayload["active"].(map[string]any) | |||||
| if !ok { | |||||
| t.Fatalf("expected ingest.active map, got %T", ingestPayload["active"]) | |||||
| } | |||||
| origin, ok := active["origin"].(map[string]any) | |||||
| if !ok { | |||||
| t.Fatalf("expected ingest.active.origin map, got %T", active["origin"]) | |||||
| } | |||||
| if origin["kind"] != "url" { | |||||
| t.Fatalf("origin kind mismatch: got %v", origin["kind"]) | |||||
| } | |||||
| if origin["endpoint"] != "http://example.org/live" { | |||||
| t.Fatalf("origin endpoint mismatch: got %v", origin["endpoint"]) | |||||
| } | |||||
| runtimePayload, ok := ingestPayload["runtime"].(map[string]any) | runtimePayload, ok := ingestPayload["runtime"].(map[string]any) | ||||
| if !ok { | if !ok { | ||||
| t.Fatalf("expected ingest.runtime map, got %T", ingestPayload["runtime"]) | t.Fatalf("expected ingest.runtime map, got %T", ingestPayload["runtime"]) | ||||
| @@ -36,12 +36,20 @@ func WithDetail(detail string) Option { | |||||
| } | } | ||||
| } | } | ||||
| func WithOrigin(origin ingest.SourceOrigin) Option { | |||||
| return func(s *Source) { | |||||
| clone := origin | |||||
| s.origin = &clone | |||||
| } | |||||
| } | |||||
| type Source struct { | type Source struct { | ||||
| id string | id string | ||||
| cfg aoiprxkit.Config | cfg aoiprxkit.Config | ||||
| factory ReceiverFactory | factory ReceiverFactory | ||||
| detail string | detail string | ||||
| origin *ingest.SourceOrigin | |||||
| chunks chan ingest.PCMChunk | chunks chan ingest.PCMChunk | ||||
| errs chan error | errs chan error | ||||
| @@ -100,6 +108,17 @@ func (s *Source) Descriptor() ingest.SourceDescriptor { | |||||
| if detail == "" { | if detail == "" { | ||||
| detail = fmt.Sprintf("rtp://%s:%d", s.cfg.MulticastGroup, s.cfg.Port) | detail = fmt.Sprintf("rtp://%s:%d", s.cfg.MulticastGroup, s.cfg.Port) | ||||
| } | } | ||||
| origin := s.origin | |||||
| if origin == nil { | |||||
| origin = &ingest.SourceOrigin{ | |||||
| Kind: "manual", | |||||
| } | |||||
| } | |||||
| if origin.Endpoint == "" { | |||||
| copyOrigin := *origin | |||||
| copyOrigin.Endpoint = fmt.Sprintf("rtp://%s:%d", s.cfg.MulticastGroup, s.cfg.Port) | |||||
| origin = ©Origin | |||||
| } | |||||
| return ingest.SourceDescriptor{ | return ingest.SourceDescriptor{ | ||||
| ID: s.id, | ID: s.id, | ||||
| Kind: "aes67", | Kind: "aes67", | ||||
| @@ -109,6 +128,7 @@ func (s *Source) Descriptor() ingest.SourceDescriptor { | |||||
| Channels: s.cfg.Channels, | Channels: s.cfg.Channels, | ||||
| SampleRateHz: s.cfg.SampleRateHz, | SampleRateHz: s.cfg.SampleRateHz, | ||||
| Detail: detail, | Detail: detail, | ||||
| Origin: origin, | |||||
| } | } | ||||
| } | } | ||||
| @@ -118,12 +118,25 @@ func TestSourceDescriptorSupportsDetailOverride(t *testing.T) { | |||||
| Port: 5004, | Port: 5004, | ||||
| SampleRateHz: 48000, | SampleRateHz: 48000, | ||||
| Channels: 2, | Channels: 2, | ||||
| }, WithDetail("rtp://239.10.20.30:5004 (SAP s=AES67-MAIN)")) | |||||
| }, WithDetail("rtp://239.10.20.30:5004 (SAP s=AES67-MAIN)"), WithOrigin(ingest.SourceOrigin{ | |||||
| Kind: "sap-discovery", | |||||
| StreamName: "AES67-MAIN", | |||||
| Endpoint: "rtp://239.10.20.30:5004", | |||||
| })) | |||||
| desc := src.Descriptor() | desc := src.Descriptor() | ||||
| if desc.Detail != "rtp://239.10.20.30:5004 (SAP s=AES67-MAIN)" { | if desc.Detail != "rtp://239.10.20.30:5004 (SAP s=AES67-MAIN)" { | ||||
| t.Fatalf("detail=%q", desc.Detail) | t.Fatalf("detail=%q", desc.Detail) | ||||
| } | } | ||||
| if desc.Origin == nil { | |||||
| t.Fatalf("expected descriptor origin") | |||||
| } | |||||
| if desc.Origin.Kind != "sap-discovery" { | |||||
| t.Fatalf("origin kind=%q", desc.Origin.Kind) | |||||
| } | |||||
| if desc.Origin.StreamName != "AES67-MAIN" { | |||||
| t.Fatalf("origin streamName=%q", desc.Origin.StreamName) | |||||
| } | |||||
| } | } | ||||
| func readChunk(t *testing.T, ch <-chan ingest.PCMChunk) ingest.PCMChunk { | func readChunk(t *testing.T, ch <-chan ingest.PCMChunk) ingest.PCMChunk { | ||||
| @@ -7,6 +7,7 @@ import ( | |||||
| "fmt" | "fmt" | ||||
| "io" | "io" | ||||
| "net/http" | "net/http" | ||||
| "net/url" | |||||
| "strings" | "strings" | ||||
| "sync" | "sync" | ||||
| "sync/atomic" | "sync/atomic" | ||||
| @@ -115,6 +116,10 @@ func (s *Source) Descriptor() ingest.SourceDescriptor { | |||||
| Transport: "http", | Transport: "http", | ||||
| Codec: s.decoderPreference, | Codec: s.decoderPreference, | ||||
| Detail: s.url, | Detail: s.url, | ||||
| Origin: &ingest.SourceOrigin{ | |||||
| Kind: "url", | |||||
| Endpoint: redactURL(s.url), | |||||
| }, | |||||
| } | } | ||||
| } | } | ||||
| @@ -345,3 +350,18 @@ func normalizeDecoderPreference(pref string) string { | |||||
| return strings.ToLower(strings.TrimSpace(pref)) | return strings.ToLower(strings.TrimSpace(pref)) | ||||
| } | } | ||||
| } | } | ||||
| func redactURL(raw string) string { | |||||
| trimmed := strings.TrimSpace(raw) | |||||
| if trimmed == "" { | |||||
| return "" | |||||
| } | |||||
| u, err := url.Parse(trimmed) | |||||
| if err != nil || u.Host == "" { | |||||
| return trimmed | |||||
| } | |||||
| u.User = nil | |||||
| u.RawQuery = "" | |||||
| u.Fragment = "" | |||||
| return u.String() | |||||
| } | |||||
| @@ -311,6 +311,20 @@ func TestWithDecoderPreferenceFallbackAliasNormalizesToFFmpeg(t *testing.T) { | |||||
| } | } | ||||
| } | } | ||||
| func TestDescriptorOriginRedactsCredentialsAndQuery(t *testing.T) { | |||||
| src := New("ice-test", "http://user:secret@example.org:8000/live.mp3?token=abc", nil, ReconnectConfig{}) | |||||
| desc := src.Descriptor() | |||||
| if desc.Origin == nil { | |||||
| t.Fatalf("expected descriptor origin") | |||||
| } | |||||
| if desc.Origin.Kind != "url" { | |||||
| t.Fatalf("origin kind=%q want url", desc.Origin.Kind) | |||||
| } | |||||
| if desc.Origin.Endpoint != "http://example.org:8000/live.mp3" { | |||||
| t.Fatalf("origin endpoint=%q", desc.Origin.Endpoint) | |||||
| } | |||||
| } | |||||
| func TestConnectAndRunRequestsICYAndPublishesStreamTitle(t *testing.T) { | func TestConnectAndRunRequestsICYAndPublishesStreamTitle(t *testing.T) { | ||||
| const ( | const ( | ||||
| audioPrefix = "ABCD" | audioPrefix = "ABCD" | ||||
| @@ -4,6 +4,8 @@ import ( | |||||
| "context" | "context" | ||||
| "fmt" | "fmt" | ||||
| "io" | "io" | ||||
| "net/url" | |||||
| "strings" | |||||
| "sync" | "sync" | ||||
| "sync/atomic" | "sync/atomic" | ||||
| "time" | "time" | ||||
| @@ -96,6 +98,11 @@ func (s *Source) Descriptor() ingest.SourceDescriptor { | |||||
| Channels: s.cfg.Channels, | Channels: s.cfg.Channels, | ||||
| SampleRateHz: s.cfg.SampleRateHz, | SampleRateHz: s.cfg.SampleRateHz, | ||||
| Detail: s.cfg.URL, | Detail: s.cfg.URL, | ||||
| Origin: &ingest.SourceOrigin{ | |||||
| Kind: "url", | |||||
| Endpoint: redactURL(s.cfg.URL), | |||||
| Mode: strings.TrimSpace(s.cfg.Mode), | |||||
| }, | |||||
| } | } | ||||
| } | } | ||||
| @@ -281,3 +288,18 @@ func (s *Source) emitError(err error) { | |||||
| default: | default: | ||||
| } | } | ||||
| } | } | ||||
| func redactURL(raw string) string { | |||||
| trimmed := strings.TrimSpace(raw) | |||||
| if trimmed == "" { | |||||
| return "" | |||||
| } | |||||
| u, err := url.Parse(trimmed) | |||||
| if err != nil || u.Host == "" { | |||||
| return trimmed | |||||
| } | |||||
| u.User = nil | |||||
| u.RawQuery = "" | |||||
| u.Fragment = "" | |||||
| return u.String() | |||||
| } | |||||
| @@ -35,6 +35,20 @@ func TestSourceEmitsChunksFromSRTFrames(t *testing.T) { | |||||
| return readCloser{Reader: bytes.NewReader(stream.Bytes())}, nil | return readCloser{Reader: bytes.NewReader(stream.Bytes())}, nil | ||||
| })) | })) | ||||
| desc := src.Descriptor() | |||||
| if desc.Origin == nil { | |||||
| t.Fatalf("expected descriptor origin") | |||||
| } | |||||
| if desc.Origin.Kind != "url" { | |||||
| t.Fatalf("origin kind=%q want url", desc.Origin.Kind) | |||||
| } | |||||
| if desc.Origin.Endpoint != "srt://127.0.0.1:9000" { | |||||
| t.Fatalf("origin endpoint=%q", desc.Origin.Endpoint) | |||||
| } | |||||
| if desc.Origin.Mode != "listener" { | |||||
| t.Fatalf("origin mode=%q want listener", desc.Origin.Mode) | |||||
| } | |||||
| if err := src.Start(context.Background()); err != nil { | if err := src.Start(context.Background()); err != nil { | ||||
| t.Fatalf("start: %v", err) | t.Fatalf("start: %v", err) | ||||
| } | } | ||||
| @@ -83,7 +83,7 @@ func BuildSource(cfg config.Config, deps Deps) (ingest.Source, AudioIngress, err | |||||
| src := srt.New("srt-main", srtCfg, opts...) | src := srt.New("srt-main", srtCfg, opts...) | ||||
| return src, nil, nil | return src, nil, nil | ||||
| case "aes67", "aoip", "aoip-rtp": | case "aes67", "aoip", "aoip-rtp": | ||||
| aoipCfg, detail, err := buildAES67Config(cfg, deps) | |||||
| aoipCfg, detail, origin, err := buildAES67Config(cfg, deps) | |||||
| if err != nil { | if err != nil { | ||||
| return nil, nil, err | return nil, nil, err | ||||
| } | } | ||||
| @@ -94,6 +94,9 @@ func BuildSource(cfg config.Config, deps Deps) (ingest.Source, AudioIngress, err | |||||
| if detail != "" { | if detail != "" { | ||||
| opts = append(opts, aoip.WithDetail(detail)) | opts = append(opts, aoip.WithDetail(detail)) | ||||
| } | } | ||||
| if origin != nil { | |||||
| opts = append(opts, aoip.WithOrigin(*origin)) | |||||
| } | |||||
| src := aoip.New("aes67-main", aoipCfg, opts...) | src := aoip.New("aes67-main", aoipCfg, opts...) | ||||
| return src, nil, nil | return src, nil, nil | ||||
| default: | default: | ||||
| @@ -129,7 +132,7 @@ func normalizeIngestKind(kind string) string { | |||||
| return strings.ToLower(strings.TrimSpace(kind)) | return strings.ToLower(strings.TrimSpace(kind)) | ||||
| } | } | ||||
| func buildAES67Config(cfg config.Config, deps Deps) (aoiprxkit.Config, string, error) { | |||||
| func buildAES67Config(cfg config.Config, deps Deps) (aoiprxkit.Config, string, *ingest.SourceOrigin, error) { | |||||
| base := aoiprxkit.DefaultConfig() | base := aoiprxkit.DefaultConfig() | ||||
| ing := cfg.Ingest.AES67 | ing := cfg.Ingest.AES67 | ||||
| if strings.TrimSpace(ing.InterfaceName) != "" { | if strings.TrimSpace(ing.InterfaceName) != "" { | ||||
| @@ -157,25 +160,32 @@ func buildAES67Config(cfg config.Config, deps Deps) (aoiprxkit.Config, string, e | |||||
| base.ReadBufferBytes = ing.ReadBufferBytes | base.ReadBufferBytes = ing.ReadBufferBytes | ||||
| } | } | ||||
| sdpText, discoveredStreamName, err := resolveAES67SDP(ing, deps) | |||||
| sdpText, discoveredStreamName, origin, err := resolveAES67SDP(ing, deps) | |||||
| if err != nil { | if err != nil { | ||||
| return aoiprxkit.Config{}, "", err | |||||
| return aoiprxkit.Config{}, "", nil, err | |||||
| } | } | ||||
| if sdpText != "" { | if sdpText != "" { | ||||
| info, err := aoiprxkit.ParseMinimalSDP(sdpText) | info, err := aoiprxkit.ParseMinimalSDP(sdpText) | ||||
| if err != nil { | if err != nil { | ||||
| return aoiprxkit.Config{}, "", fmt.Errorf("parse ingest.aes67 SDP: %w", err) | |||||
| return aoiprxkit.Config{}, "", nil, fmt.Errorf("parse ingest.aes67 SDP: %w", err) | |||||
| } | } | ||||
| parsed, err := aoiprxkit.ConfigFromSDP(base, info) | parsed, err := aoiprxkit.ConfigFromSDP(base, info) | ||||
| if err != nil { | if err != nil { | ||||
| return aoiprxkit.Config{}, "", fmt.Errorf("map ingest.aes67 SDP: %w", err) | |||||
| return aoiprxkit.Config{}, "", nil, fmt.Errorf("map ingest.aes67 SDP: %w", err) | |||||
| } | } | ||||
| detail := "" | detail := "" | ||||
| endpoint := fmt.Sprintf("rtp://%s:%d", parsed.MulticastGroup, parsed.Port) | |||||
| if discoveredStreamName != "" { | if discoveredStreamName != "" { | ||||
| detail = fmt.Sprintf("rtp://%s:%d (SAP s=%s)", parsed.MulticastGroup, parsed.Port, discoveredStreamName) | detail = fmt.Sprintf("rtp://%s:%d (SAP s=%s)", parsed.MulticastGroup, parsed.Port, discoveredStreamName) | ||||
| } | } | ||||
| return parsed, detail, nil | |||||
| if origin == nil { | |||||
| origin = &ingest.SourceOrigin{} | |||||
| } | |||||
| if origin.Endpoint == "" { | |||||
| origin.Endpoint = endpoint | |||||
| } | |||||
| return parsed, detail, origin, nil | |||||
| } | } | ||||
| if strings.TrimSpace(ing.MulticastGroup) != "" { | if strings.TrimSpace(ing.MulticastGroup) != "" { | ||||
| base.MulticastGroup = strings.TrimSpace(ing.MulticastGroup) | base.MulticastGroup = strings.TrimSpace(ing.MulticastGroup) | ||||
| @@ -184,27 +194,42 @@ func buildAES67Config(cfg config.Config, deps Deps) (aoiprxkit.Config, string, e | |||||
| base.Port = ing.Port | base.Port = ing.Port | ||||
| } | } | ||||
| if err := base.Validate(); err != nil { | if err := base.Validate(); err != nil { | ||||
| return aoiprxkit.Config{}, "", err | |||||
| return aoiprxkit.Config{}, "", nil, err | |||||
| } | |||||
| if origin == nil { | |||||
| origin = &ingest.SourceOrigin{Kind: "manual"} | |||||
| } | |||||
| if origin.Endpoint == "" { | |||||
| origin.Endpoint = fmt.Sprintf("rtp://%s:%d", base.MulticastGroup, base.Port) | |||||
| } | } | ||||
| return base, "", nil | |||||
| return base, "", origin, nil | |||||
| } | } | ||||
| func resolveAES67SDP(ing config.IngestAES67Config, deps Deps) (string, string, error) { | |||||
| func resolveAES67SDP(ing config.IngestAES67Config, deps Deps) (string, string, *ingest.SourceOrigin, error) { | |||||
| sdpText := strings.TrimSpace(ing.SDP) | sdpText := strings.TrimSpace(ing.SDP) | ||||
| if sdpText == "" && strings.TrimSpace(ing.SDPPath) != "" { | if sdpText == "" && strings.TrimSpace(ing.SDPPath) != "" { | ||||
| data, err := os.ReadFile(filepath.Clean(ing.SDPPath)) | |||||
| sdpPath := filepath.Clean(ing.SDPPath) | |||||
| data, err := os.ReadFile(sdpPath) | |||||
| if err != nil { | if err != nil { | ||||
| return "", "", fmt.Errorf("read ingest.aes67.sdpPath: %w", err) | |||||
| return "", "", nil, fmt.Errorf("read ingest.aes67.sdpPath: %w", err) | |||||
| } | } | ||||
| sdpText = string(data) | sdpText = string(data) | ||||
| return sdpText, "", &ingest.SourceOrigin{ | |||||
| Kind: "sdp-file", | |||||
| SDPPath: sdpPath, | |||||
| }, nil | |||||
| } | } | ||||
| if sdpText != "" { | if sdpText != "" { | ||||
| return sdpText, "", nil | |||||
| return sdpText, "", &ingest.SourceOrigin{ | |||||
| Kind: "sdp-inline", | |||||
| }, nil | |||||
| } | } | ||||
| discoveryEnabled := ing.Discovery.Enabled || strings.TrimSpace(ing.Discovery.StreamName) != "" | discoveryEnabled := ing.Discovery.Enabled || strings.TrimSpace(ing.Discovery.StreamName) != "" | ||||
| if !discoveryEnabled { | if !discoveryEnabled { | ||||
| return "", "", nil | |||||
| return "", "", &ingest.SourceOrigin{ | |||||
| Kind: "manual", | |||||
| }, nil | |||||
| } | } | ||||
| timeout := time.Duration(ing.Discovery.TimeoutMs) * time.Millisecond | timeout := time.Duration(ing.Discovery.TimeoutMs) * time.Millisecond | ||||
| if timeout <= 0 { | if timeout <= 0 { | ||||
| @@ -223,12 +248,15 @@ func resolveAES67SDP(ing config.IngestAES67Config, deps Deps) (string, string, e | |||||
| } | } | ||||
| announcement, err := discover(context.Background(), req) | announcement, err := discover(context.Background(), req) | ||||
| if err != nil { | if err != nil { | ||||
| return "", "", fmt.Errorf("discover ingest.aes67 stream %q via SAP: %w", req.StreamName, err) | |||||
| return "", "", nil, fmt.Errorf("discover ingest.aes67 stream %q via SAP: %w", req.StreamName, err) | |||||
| } | } | ||||
| if strings.TrimSpace(announcement.SDP) == "" { | if strings.TrimSpace(announcement.SDP) == "" { | ||||
| return "", "", fmt.Errorf("discover ingest.aes67 stream %q via SAP: empty SDP payload", req.StreamName) | |||||
| return "", "", nil, fmt.Errorf("discover ingest.aes67 stream %q via SAP: empty SDP payload", req.StreamName) | |||||
| } | } | ||||
| return announcement.SDP, req.StreamName, nil | |||||
| return announcement.SDP, req.StreamName, &ingest.SourceOrigin{ | |||||
| Kind: "sap-discovery", | |||||
| StreamName: req.StreamName, | |||||
| }, nil | |||||
| } | } | ||||
| func discoverAES67ViaSAP(ctx context.Context, req AES67DiscoverRequest) (aoiprxkit.SAPAnnouncement, error) { | func discoverAES67ViaSAP(ctx context.Context, req AES67DiscoverRequest) (aoiprxkit.SAPAnnouncement, error) { | ||||
| @@ -90,6 +90,9 @@ func TestBuildSourceIcecastUsesDecoderPreference(t *testing.T) { | |||||
| if got := src.Descriptor().Codec; got != "ffmpeg" { | if got := src.Descriptor().Codec; got != "ffmpeg" { | ||||
| t.Fatalf("codec=%s want ffmpeg", got) | t.Fatalf("codec=%s want ffmpeg", got) | ||||
| } | } | ||||
| if got := src.Descriptor().Origin; got == nil || got.Kind != "url" { | |||||
| t.Fatalf("expected icecast origin kind url, got %+v", got) | |||||
| } | |||||
| } | } | ||||
| func TestBuildSourceSRT(t *testing.T) { | func TestBuildSourceSRT(t *testing.T) { | ||||
| @@ -113,6 +116,9 @@ func TestBuildSourceSRT(t *testing.T) { | |||||
| if got := src.Descriptor().Kind; got != "srt" { | if got := src.Descriptor().Kind; got != "srt" { | ||||
| t.Fatalf("source kind=%s", got) | t.Fatalf("source kind=%s", got) | ||||
| } | } | ||||
| if got := src.Descriptor().Origin; got == nil || got.Kind != "url" || got.Mode != "listener" { | |||||
| t.Fatalf("expected srt origin url/listener, got %+v", got) | |||||
| } | |||||
| } | } | ||||
| func TestBuildSourceAES67(t *testing.T) { | func TestBuildSourceAES67(t *testing.T) { | ||||
| @@ -159,6 +165,12 @@ func TestBuildSourceAES67FromInlineSDP(t *testing.T) { | |||||
| if desc.SampleRateHz != 48000 || desc.Channels != 2 { | if desc.SampleRateHz != 48000 || desc.Channels != 2 { | ||||
| t.Fatalf("shape=%d/%d", desc.SampleRateHz, desc.Channels) | t.Fatalf("shape=%d/%d", desc.SampleRateHz, desc.Channels) | ||||
| } | } | ||||
| if desc.Origin == nil || desc.Origin.Kind != "sdp-inline" { | |||||
| t.Fatalf("origin=%+v want sdp-inline", desc.Origin) | |||||
| } | |||||
| if desc.Origin.Endpoint != "rtp://239.10.20.30:5004" { | |||||
| t.Fatalf("origin endpoint=%q", desc.Origin.Endpoint) | |||||
| } | |||||
| } | } | ||||
| func TestBuildSourceAES67WithDiscovery(t *testing.T) { | func TestBuildSourceAES67WithDiscovery(t *testing.T) { | ||||
| @@ -191,6 +203,12 @@ func TestBuildSourceAES67WithDiscovery(t *testing.T) { | |||||
| if desc.Detail != "rtp://239.10.20.30:5004 (SAP s=AES67-MAIN)" { | if desc.Detail != "rtp://239.10.20.30:5004 (SAP s=AES67-MAIN)" { | ||||
| t.Fatalf("descriptor detail=%q", desc.Detail) | t.Fatalf("descriptor detail=%q", desc.Detail) | ||||
| } | } | ||||
| if desc.Origin == nil || desc.Origin.Kind != "sap-discovery" { | |||||
| t.Fatalf("origin=%+v want sap-discovery", desc.Origin) | |||||
| } | |||||
| if desc.Origin.StreamName != "AES67-MAIN" { | |||||
| t.Fatalf("origin streamName=%q", desc.Origin.StreamName) | |||||
| } | |||||
| } | } | ||||
| func TestBuildSourceAES67DiscoveryError(t *testing.T) { | func TestBuildSourceAES67DiscoveryError(t *testing.T) { | ||||
| @@ -15,12 +15,23 @@ type PCMChunk struct { | |||||
| } | } | ||||
| type SourceDescriptor struct { | type SourceDescriptor struct { | ||||
| ID string `json:"id"` | |||||
| Kind string `json:"kind"` | |||||
| Family string `json:"family"` | |||||
| Transport string `json:"transport"` | |||||
| Codec string `json:"codec"` | |||||
| Channels int `json:"channels"` | |||||
| SampleRateHz int `json:"sampleRateHz"` | |||||
| Detail string `json:"detail,omitempty"` | |||||
| ID string `json:"id"` | |||||
| Kind string `json:"kind"` | |||||
| Family string `json:"family"` | |||||
| Transport string `json:"transport"` | |||||
| Codec string `json:"codec"` | |||||
| Channels int `json:"channels"` | |||||
| SampleRateHz int `json:"sampleRateHz"` | |||||
| Detail string `json:"detail,omitempty"` | |||||
| Origin *SourceOrigin `json:"origin,omitempty"` | |||||
| } | |||||
| // SourceOrigin describes where an ingest source definition came from and | |||||
| // which endpoint it resolved to, so control/runtime can show provenance. | |||||
| type SourceOrigin struct { | |||||
| Kind string `json:"kind,omitempty"` | |||||
| Endpoint string `json:"endpoint,omitempty"` | |||||
| Mode string `json:"mode,omitempty"` | |||||
| StreamName string `json:"streamName,omitempty"` | |||||
| SDPPath string `json:"sdpPath,omitempty"` | |||||
| } | } | ||||