diff --git a/app/webrtc/ffmpeg_args.go b/app/webrtc/ffmpeg_args.go index 14f8e6e..1b99dc1 100644 --- a/app/webrtc/ffmpeg_args.go +++ b/app/webrtc/ffmpeg_args.go @@ -32,7 +32,16 @@ func BuildArgs(cfg appcfg.ConfigWebRTC, videoPort int) []string { acopy = []string{"-c:a", "libopus", "-b:a", "96k"} } - args := []string{"-map", "0:v:0"} + videoMap := cfg.VideoMap + if videoMap == "" { + videoMap = "0:v:0" + } + audioMap := cfg.AudioMap + if audioMap == "" { + audioMap = "0:a:0" + } + + args := []string{"-map", videoMap} args = append(args, vcopy...) args = append(args, "-payload_type", fmt.Sprint(cfg.VideoPT), @@ -40,7 +49,7 @@ func BuildArgs(cfg appcfg.ConfigWebRTC, videoPort int) []string { fmt.Sprintf("udp://127.0.0.1:%d?pkt_size=1316", videoPort), ) - args = append(args, "-map", "0:a:0") + args = append(args, "-map", audioMap) args = append(args, acopy...) args = append(args, "-payload_type", fmt.Sprint(cfg.AudioPT), diff --git a/app/webrtc/ffmpeg_args_test.go b/app/webrtc/ffmpeg_args_test.go index 7e1cb52..6fec28d 100644 --- a/app/webrtc/ffmpeg_args_test.go +++ b/app/webrtc/ffmpeg_args_test.go @@ -87,3 +87,46 @@ func any(haystack []string, prefix string) bool { } return false } + +// TestBuildArgs_DefaultMaps confirms 0:v:0 / 0:a:0 are emitted when +// VideoMap / AudioMap are empty (regression on the fix for issue #2 — +// the prior version had these as hardcoded literals; if VideoMap is +// ever empty unexpectedly, BuildArgs must still produce a working +// command line). +func TestBuildArgs_DefaultMaps(t *testing.T) { + cfg := appcfg.ConfigWebRTC{Enabled: true, VideoPT: 102, AudioPT: 111} + got := BuildArgs(cfg, 50000) + if !contains(got, "-map", "0:v:0") { + t.Fatalf("expected default video map 0:v:0, got %v", got) + } + if !contains(got, "-map", "0:a:0") { + t.Fatalf("expected default audio map 0:a:0, got %v", got) + } +} + +// TestBuildArgs_CustomMaps drives the issue-#2 fix: when the user +// configures a multi-input pipeline (audio on input #1, etc.), the +// emitted -map values must follow the user's choice rather than the +// "0:v:0"/"0:a:0" assumption. +func TestBuildArgs_CustomMaps(t *testing.T) { + cfg := appcfg.ConfigWebRTC{ + Enabled: true, + VideoPT: 102, + AudioPT: 111, + VideoMap: "0:v:1", + AudioMap: "1:a:0", + } + got := BuildArgs(cfg, 50000) + if !contains(got, "-map", "0:v:1") { + t.Fatalf("expected custom video map 0:v:1, got %v", got) + } + if !contains(got, "-map", "1:a:0") { + t.Fatalf("expected custom audio map 1:a:0, got %v", got) + } + // The default literals should NOT appear when overridden. + for _, opt := range got { + if opt == "0:v:0" || opt == "0:a:0" { + t.Errorf("expected no default maps in output, found %q in %v", opt, got) + } + } +} diff --git a/http/api/process.go b/http/api/process.go index c0df57d..5a4e115 100644 --- a/http/api/process.go +++ b/http/api/process.go @@ -44,10 +44,12 @@ type ProcessConfigLimits struct { // ProcessConfig represents the configuration of an ffmpeg process type ProcessConfigWebRTC struct { - Enabled bool `json:"enabled"` - VideoPT uint8 `json:"video_pt,omitempty"` - AudioPT uint8 `json:"audio_pt,omitempty"` - ForceTranscode bool `json:"force_transcode,omitempty"` + Enabled bool `json:"enabled"` + VideoPT uint8 `json:"video_pt,omitempty"` + AudioPT uint8 `json:"audio_pt,omitempty"` + ForceTranscode bool `json:"force_transcode,omitempty"` + VideoMap string `json:"video_map,omitempty"` + AudioMap string `json:"audio_map,omitempty"` } type ProcessConfig struct { @@ -83,6 +85,8 @@ func (cfg *ProcessConfig) Marshal() *app.Config { VideoPT: cfg.WebRTC.VideoPT, AudioPT: cfg.WebRTC.AudioPT, ForceTranscode: cfg.WebRTC.ForceTranscode, + VideoMap: cfg.WebRTC.VideoMap, + AudioMap: cfg.WebRTC.AudioMap, }, } @@ -168,6 +172,8 @@ func (cfg *ProcessConfig) Unmarshal(c *app.Config) { cfg.WebRTC.VideoPT = c.WebRTC.VideoPT cfg.WebRTC.AudioPT = c.WebRTC.AudioPT cfg.WebRTC.ForceTranscode = c.WebRTC.ForceTranscode + cfg.WebRTC.VideoMap = c.WebRTC.VideoMap + cfg.WebRTC.AudioMap = c.WebRTC.AudioMap cfg.Options = make([]string, len(c.Options)) copy(cfg.Options, c.Options) diff --git a/http/api/process_webrtc_test.go b/http/api/process_webrtc_test.go index d6bed2f..57050da 100644 --- a/http/api/process_webrtc_test.go +++ b/http/api/process_webrtc_test.go @@ -79,3 +79,31 @@ func TestProcessConfigWebRTCDefaults(t *testing.T) { t.Fatalf("default should be disabled, got %+v", cfg.WebRTC) } } + +// TestProcessConfigWebRTCMapsRoundtrip extends the WebRTC DTO +// roundtrip with the issue-#2 VideoMap/AudioMap fields so the +// regression doesn't repeat: a multi-input pipeline that sets +// `audio_map: "1:a:0"` must reach the restream config layer +// unchanged. +func TestProcessConfigWebRTCMapsRoundtrip(t *testing.T) { + body := []byte(`{ + "id":"p","input":[{"id":"i","address":"x"}],"output":[{"id":"o","address":"-"}], + "webrtc":{"enabled":true,"video_map":"0:v:1","audio_map":"1:a:0"} + }`) + var dto ProcessConfig + if err := json.Unmarshal(body, &dto); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if dto.WebRTC.VideoMap != "0:v:1" || dto.WebRTC.AudioMap != "1:a:0" { + t.Fatalf("DTO maps lost: %+v", dto.WebRTC) + } + cfg := dto.Marshal() + if cfg.WebRTC.VideoMap != "0:v:1" || cfg.WebRTC.AudioMap != "1:a:0" { + t.Fatalf("app.Config maps lost: %+v", cfg.WebRTC) + } + var back ProcessConfig + back.Unmarshal(cfg) + if back.WebRTC.VideoMap != "0:v:1" || back.WebRTC.AudioMap != "1:a:0" { + t.Fatalf("Unmarshal lost maps: %+v", back.WebRTC) + } +} diff --git a/restream/app/process.go b/restream/app/process.go index 4791871..8381e8d 100644 --- a/restream/app/process.go +++ b/restream/app/process.go @@ -25,10 +25,18 @@ type ConfigIO struct { // RTP to a loopback UDP port the subsystem allocates. The subsystem reads // that RTP and fans it out to WHEP subscribers. type ConfigWebRTC struct { - Enabled bool `json:"enabled"` - VideoPT uint8 `json:"video_pt"` - AudioPT uint8 `json:"audio_pt"` - ForceTranscode bool `json:"force_transcode"` + Enabled bool `json:"enabled"` + VideoPT uint8 `json:"video_pt"` + AudioPT uint8 `json:"audio_pt"` + ForceTranscode bool `json:"force_transcode"` + + // VideoMap / AudioMap select which input stream the WebRTC RTP + // legs draw from. Defaults are "0:v:0" and "0:a:0" — correct for + // any RTMP / SRT publisher (single input, both A and V on input + // 0). For multi-input pipelines (lavfi test sources, SDI capture + // fed alongside file audio, etc.) the operator can override. + VideoMap string `json:"video_map,omitempty"` + AudioMap string `json:"audio_map,omitempty"` } // Clone returns a deep copy of the WebRTC config (currently a value copy;