diff --git a/http/api/process.go b/http/api/process.go index d52d439..c0df57d 100644 --- a/http/api/process.go +++ b/http/api/process.go @@ -43,18 +43,26 @@ 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"` +} + type ProcessConfig struct { - ID string `json:"id"` - Type string `json:"type" validate:"oneof='ffmpeg' ''" jsonschema:"enum=ffmpeg,enum="` - Reference string `json:"reference"` - Input []ProcessConfigIO `json:"input" validate:"required"` - Output []ProcessConfigIO `json:"output" validate:"required"` - Options []string `json:"options"` - Reconnect bool `json:"reconnect"` - ReconnectDelay uint64 `json:"reconnect_delay_seconds" format:"uint64"` - Autostart bool `json:"autostart"` - StaleTimeout uint64 `json:"stale_timeout_seconds" format:"uint64"` - Limits ProcessConfigLimits `json:"limits"` + ID string `json:"id"` + Type string `json:"type" validate:"oneof='ffmpeg' ''" jsonschema:"enum=ffmpeg,enum="` + Reference string `json:"reference"` + Input []ProcessConfigIO `json:"input" validate:"required"` + Output []ProcessConfigIO `json:"output" validate:"required"` + Options []string `json:"options"` + Reconnect bool `json:"reconnect"` + ReconnectDelay uint64 `json:"reconnect_delay_seconds" format:"uint64"` + Autostart bool `json:"autostart"` + StaleTimeout uint64 `json:"stale_timeout_seconds" format:"uint64"` + Limits ProcessConfigLimits `json:"limits"` + WebRTC ProcessConfigWebRTC `json:"webrtc"` } // Marshal converts a process config in API representation to a restreamer process config @@ -70,6 +78,12 @@ func (cfg *ProcessConfig) Marshal() *app.Config { LimitCPU: cfg.Limits.CPU, LimitMemory: cfg.Limits.Memory * 1024 * 1024, LimitWaitFor: cfg.Limits.WaitFor, + WebRTC: app.ConfigWebRTC{ + Enabled: cfg.WebRTC.Enabled, + VideoPT: cfg.WebRTC.VideoPT, + AudioPT: cfg.WebRTC.AudioPT, + ForceTranscode: cfg.WebRTC.ForceTranscode, + }, } cfg.generateInputOutputIDs(cfg.Input) @@ -150,6 +164,11 @@ func (cfg *ProcessConfig) Unmarshal(c *app.Config) { cfg.Limits.Memory = c.LimitMemory / 1024 / 1024 cfg.Limits.WaitFor = c.LimitWaitFor + cfg.WebRTC.Enabled = c.WebRTC.Enabled + cfg.WebRTC.VideoPT = c.WebRTC.VideoPT + cfg.WebRTC.AudioPT = c.WebRTC.AudioPT + cfg.WebRTC.ForceTranscode = c.WebRTC.ForceTranscode + 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 new file mode 100644 index 0000000..d6bed2f --- /dev/null +++ b/http/api/process_webrtc_test.go @@ -0,0 +1,81 @@ +package api + +import ( + "encoding/json" + "testing" + + "github.com/datarhei/core/v16/restream/app" +) + +// TestProcessConfigWebRTCRoundtrip locks down the API DTO ↔ restream +// app.Config mapping for the per-process WebRTC block. +// +// Regression: the M2 cut shipped without WebRTC on ProcessConfig, so +// JSON arriving at POST /api/v3/process was silently stripped of +// `webrtc.enabled`, the restream config never saw it, the start hook +// never bound a Source, and WHEP returned 404. This test fails on the +// pre-fix code (Marshal would yield `app.ConfigWebRTC{}`) and passes +// once the DTO carries the field. +func TestProcessConfigWebRTCRoundtrip(t *testing.T) { + // 1. JSON in → DTO → app.Config + body := []byte(`{ + "id":"p","input":[{"id":"i","address":"x"}],"output":[{"id":"o","address":"-"}], + "webrtc":{"enabled":true,"video_pt":102,"audio_pt":111,"force_transcode":true} + }`) + var dto ProcessConfig + if err := json.Unmarshal(body, &dto); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !dto.WebRTC.Enabled { + t.Fatalf("DTO.WebRTC.Enabled lost on JSON decode: %+v", dto.WebRTC) + } + cfg := dto.Marshal() + if !cfg.WebRTC.Enabled || cfg.WebRTC.VideoPT != 102 || cfg.WebRTC.AudioPT != 111 || !cfg.WebRTC.ForceTranscode { + t.Fatalf("app.Config.WebRTC mapped wrong: %+v", cfg.WebRTC) + } + + // 2. app.Config → DTO → JSON out + stored := &app.Config{ + ID: "p", + Input: []app.ConfigIO{{ID: "i", Address: "x"}}, + Output: []app.ConfigIO{{ID: "o", Address: "-"}}, + WebRTC: app.ConfigWebRTC{ + Enabled: true, + VideoPT: 102, + AudioPT: 111, + ForceTranscode: true, + }, + } + var dto2 ProcessConfig + dto2.Unmarshal(stored) + if !dto2.WebRTC.Enabled || dto2.WebRTC.VideoPT != 102 { + t.Fatalf("Unmarshal lost WebRTC: %+v", dto2.WebRTC) + } + out, err := json.Marshal(dto2) + if err != nil { + t.Fatalf("marshal: %v", err) + } + // Decode again and compare. + var dto3 ProcessConfig + if err := json.Unmarshal(out, &dto3); err != nil { + t.Fatalf("re-unmarshal: %v", err) + } + if dto3.WebRTC != dto.WebRTC { + t.Fatalf("roundtrip diverged: in=%+v out=%+v", dto.WebRTC, dto3.WebRTC) + } +} + +// TestProcessConfigWebRTCDefaults: when "webrtc" is absent in the +// inbound JSON, Marshal must still produce a valid app.Config — the +// zero ConfigWebRTC means "disabled" and the start hook should no-op. +func TestProcessConfigWebRTCDefaults(t *testing.T) { + body := []byte(`{"id":"p","input":[{"id":"i","address":"x"}],"output":[{"id":"o","address":"-"}]}`) + var dto ProcessConfig + if err := json.Unmarshal(body, &dto); err != nil { + t.Fatalf("unmarshal: %v", err) + } + cfg := dto.Marshal() + if cfg.WebRTC.Enabled { + t.Fatalf("default should be disabled, got %+v", cfg.WebRTC) + } +}