diff --git a/README.md b/README.md index 5aafd9e..07d65b4 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,10 @@ logfile: enabled: true # defaults to false path: /path/to/logfile # defaults to $XDG_STATE_HOME/octoplex/octoplex.log sources: - rtmp: - enabled: true # must be true + mediaServer: streamKey: live # defaults to "live" host: rtmp.example.com # defaults to "localhost" - bindAddr: # optional + rtmp: # must be present, use `rtmp: {}` for defaults ip: 0.0.0.0 # defaults to 127.0.0.1 port: 1935 # defaults to 1935 destinations: @@ -115,8 +114,8 @@ destinations: :information_source: It is also possible to add and remove destinations directly from the terminal user interface. -:warning: `sources.rtmp.bindAddr.ip` must be set to a valid IP address if you want -to accept connections from other hosts. Leave it blank to bind only to +:warning: `sources.mediaServer.rtmp.ip` must be set to a valid IP address if +you want to accept connections from other hosts. Leave it blank to bind only to localhost (`127.0.0.1`) or use `0.0.0.0` to bind to all network interfaces. ## Contributing diff --git a/internal/app/app.go b/internal/app/app.go index e1223ad..68c9216 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -39,8 +39,8 @@ func Run(ctx context.Context, params RunParams) error { applyConfig(cfg, state) // While RTMP is the only source, it doesn't make sense to disable it. - if !cfg.Sources.RTMP.Enabled { - return errors.New("config: sources.rtmp.enabled must be set to true") + if cfg.Sources.MediaServer.RTMP == nil { + return errors.New("config: sources.mediaServer.rtmp is required") } logger := params.Logger @@ -89,9 +89,9 @@ func Run(ctx context.Context, params RunParams) error { updateUI() srv, err := mediaserver.NewActor(ctx, mediaserver.NewActorParams{ - RTMPAddr: domain.NetAddr(cfg.Sources.RTMP.BindAddr), - RTMPHost: cfg.Sources.RTMP.Host, - StreamKey: mediaserver.StreamKey(cfg.Sources.RTMP.StreamKey), + RTMPAddr: domain.NetAddr(cfg.Sources.MediaServer.RTMP.NetAddr), + Host: cfg.Sources.MediaServer.Host, + StreamKey: mediaserver.StreamKey(cfg.Sources.MediaServer.StreamKey), ContainerClient: containerClient, Logger: logger.With("component", "mediaserver"), }) diff --git a/internal/app/integration_test.go b/internal/app/integration_test.go index 6b03e86..3342ec6 100644 --- a/internal/app/integration_test.go +++ b/internal/app/integration_test.go @@ -78,12 +78,13 @@ func testIntegration(t *testing.T, rtmpHost string, rtmpIP string, rtmpPort int, destURL2 := fmt.Sprintf("rtmp://%s:%d/%s/dest2", hostIP, destServerPort.Int(), wantStreamKey) configService := setupConfigService(t, config.Config{ Sources: config.Sources{ - RTMP: config.RTMPSource{ - Enabled: true, + MediaServer: config.MediaServerSource{ Host: rtmpHost, - BindAddr: config.NetAddr{IP: rtmpIP, Port: rtmpPort}, StreamKey: streamKey, - }}, + RTMP: &config.RTMPSource{ + NetAddr: config.NetAddr{IP: rtmpIP, Port: rtmpPort}, + }}, + }, // Load one destination from config, add the other in-app. Destinations: []config.Destination{{Name: "Local server 1", URL: destURL1}}, }) @@ -275,9 +276,9 @@ func TestIntegrationCustomRTMPURL(t *testing.T) { configService := setupConfigService(t, config.Config{ Sources: config.Sources{ - RTMP: config.RTMPSource{ - Enabled: true, - Host: "rtmp.live.tv", + MediaServer: config.MediaServerSource{ + Host: "rtmp.live.tv", + RTMP: &config.RTMPSource{}, }, }, }) @@ -336,7 +337,7 @@ func TestIntegrationRestartDestination(t *testing.T) { screen, screenCaptureC, getContents := setupSimulationScreen(t) configService := setupConfigService(t, config.Config{ - Sources: config.Sources{RTMP: config.RTMPSource{Enabled: true}}, + Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: &config.RTMPSource{}}}, Destinations: []config.Destination{{ Name: "Local server 1", URL: fmt.Sprintf("rtmp://%s:%d/live", hostIP, destServerRTMPPort.Int()), @@ -482,7 +483,7 @@ func TestIntegrationStartDestinationFailed(t *testing.T) { screen, screenCaptureC, getContents := setupSimulationScreen(t) configService := setupConfigService(t, config.Config{ - Sources: config.Sources{RTMP: config.RTMPSource{Enabled: true}}, + Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: &config.RTMPSource{}}}, Destinations: []config.Destination{{Name: "Example server", URL: "rtmp://rtmp.example.com/live"}}, }) @@ -558,7 +559,7 @@ func TestIntegrationDestinationValidations(t *testing.T) { screen, screenCaptureC, getContents := setupSimulationScreen(t) configService := setupConfigService(t, config.Config{ - Sources: config.Sources{RTMP: config.RTMPSource{Enabled: true, StreamKey: "live"}}, + Sources: config.Sources{MediaServer: config.MediaServerSource{StreamKey: "live", RTMP: &config.RTMPSource{}}}, }) done := make(chan struct{}) @@ -701,7 +702,7 @@ func TestIntegrationStartupCheck(t *testing.T) { dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) require.NoError(t, err) - configService := setupConfigService(t, config.Config{Sources: config.Sources{RTMP: config.RTMPSource{Enabled: true}}}) + configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: &config.RTMPSource{}}}}) screen, screenCaptureC, getContents := setupSimulationScreen(t) done := make(chan struct{}) @@ -770,7 +771,7 @@ func TestIntegrationMediaServerError(t *testing.T) { dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) require.NoError(t, err) - configService := setupConfigService(t, config.Config{Sources: config.Sources{RTMP: config.RTMPSource{Enabled: true}}}) + configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: &config.RTMPSource{}}}}) screen, screenCaptureC, getContents := setupSimulationScreen(t) done := make(chan struct{}) @@ -809,7 +810,7 @@ func TestIntegrationDockerClientError(t *testing.T) { var dockerClient mocks.DockerClient dockerClient.EXPECT().NetworkCreate(mock.Anything, mock.Anything, mock.Anything).Return(network.CreateResponse{}, errors.New("boom")) - configService := setupConfigService(t, config.Config{Sources: config.Sources{RTMP: config.RTMPSource{Enabled: true}}}) + configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: &config.RTMPSource{}}}}) screen, screenCaptureC, getContents := setupSimulationScreen(t) done := make(chan struct{}) @@ -850,7 +851,7 @@ func TestIntegrationDockerConnectionError(t *testing.T) { dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.WithHost("http://docker.example.com")) require.NoError(t, err) - configService := setupConfigService(t, config.Config{Sources: config.Sources{RTMP: config.RTMPSource{Enabled: true}}}) + configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: &config.RTMPSource{}}}}) screen, screenCaptureC, getContents := setupSimulationScreen(t) done := make(chan struct{}) diff --git a/internal/config/config.go b/internal/config/config.go index 535e044..3abbda4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,15 +30,19 @@ type NetAddr struct { // RTMPSource holds the configuration for the RTMP source. type RTMPSource struct { - Enabled bool `yaml:"enabled"` - StreamKey string `yaml:"streamKey,omitempty"` - Host string `yaml:"host,omitempty"` - BindAddr NetAddr `yaml:"bindAddr,omitempty"` + NetAddr `yaml:",inline"` +} + +// MediaServerSource holds the configuration for the media server source. +type MediaServerSource struct { + StreamKey string `yaml:"streamKey,omitempty"` + Host string `yaml:"host,omitempty"` + RTMP *RTMPSource `yaml:"rtmp,omitempty"` } // Sources holds the configuration for the sources. type Sources struct { - RTMP RTMPSource `yaml:"rtmp"` + MediaServer MediaServerSource `yaml:"mediaServer"` } // Config holds the configuration for the application. diff --git a/internal/config/service.go b/internal/config/service.go index afec59d..3fec40c 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -182,8 +182,8 @@ func (s *Service) writeConfig(cfgBytes []byte) error { // // This function may set exported fields to arbitrary values. func (s *Service) populateConfigOnBuild(cfg *Config) { - cfg.Sources.RTMP.Enabled = true - cfg.Sources.RTMP.StreamKey = "live" + cfg.Sources.MediaServer.StreamKey = "live" + cfg.Sources.MediaServer.RTMP = &RTMPSource{NetAddr{"127.0.0.1", 1935}} s.populateConfigOnRead(cfg) } diff --git a/internal/config/service_test.go b/internal/config/service_test.go index eb0b09d..ce3dd12 100644 --- a/internal/config/service_test.go +++ b/internal/config/service_test.go @@ -44,7 +44,9 @@ func TestConfigServiceCurrent(t *testing.T) { t.Cleanup(func() { require.NoError(t, os.RemoveAll(systemConfigDir)) }) // Ensure defaults are set: - assert.True(t, service.Current().Sources.RTMP.Enabled) + assert.NotNil(t, service.Current().Sources.MediaServer.RTMP) + assert.Equal(t, "127.0.0.1", service.Current().Sources.MediaServer.RTMP.IP) + assert.Equal(t, 1935, service.Current().Sources.MediaServer.RTMP.Port) } func TestConfigServiceCreateConfig(t *testing.T) { @@ -67,7 +69,9 @@ func TestConfigServiceCreateConfig(t *testing.T) { var readCfg config.Config require.NoError(t, yaml.Unmarshal(cfgBytes, &readCfg)) - assert.True(t, readCfg.Sources.RTMP.Enabled, "default values not set") + assert.NotNil(t, readCfg.Sources.MediaServer.RTMP) + assert.Equal(t, "127.0.0.1", readCfg.Sources.MediaServer.RTMP.IP) + assert.Equal(t, 1935, readCfg.Sources.MediaServer.RTMP.Port) } func TestConfigServiceReadConfig(t *testing.T) { @@ -90,13 +94,14 @@ func TestConfigServiceReadConfig(t *testing.T) { Path: "test.log", }, Sources: config.Sources{ - RTMP: config.RTMPSource{ - Enabled: true, + MediaServer: config.MediaServerSource{ StreamKey: "s3cr3t", Host: "rtmp.example.com", - BindAddr: config.NetAddr{ - IP: "0.0.0.0", - Port: 19350, + RTMP: &config.RTMPSource{ + NetAddr: config.NetAddr{ + IP: "0.0.0.0", + Port: 19350, + }, }, }, }, diff --git a/internal/config/testdata/complete.yml b/internal/config/testdata/complete.yml index e3d7fa8..04c77c1 100644 --- a/internal/config/testdata/complete.yml +++ b/internal/config/testdata/complete.yml @@ -3,11 +3,10 @@ logfile: enabled: true path: test.log sources: - rtmp: - enabled: true + mediaServer: streamKey: s3cr3t host: rtmp.example.com - bindAddr: + rtmp: ip: 0.0.0.0 port: 19350 destinations: diff --git a/internal/mediaserver/actor.go b/internal/mediaserver/actor.go index 6fc584c..b2c883d 100644 --- a/internal/mediaserver/actor.go +++ b/internal/mediaserver/actor.go @@ -30,7 +30,7 @@ const ( defaultAPIPort = 9997 // default API host port for the media server defaultRTMPIP = "127.0.0.1" // default RTMP host IP, bound to localhost for security defaultRTMPPort = 1935 // default RTMP host port for the media server - defaultRTMPHost = "localhost" // default RTMP host name, used for the RTMP URL + defaultHost = "localhost" // default RTMP host name, used for the RTMP URL defaultChanSize = 64 // default channel size for asynchronous non-error channels imageNameMediaMTX = "ghcr.io/rfwatson/mediamtx-alpine:latest" // image name for mediamtx defaultStreamKey StreamKey = "live" // Default stream key. See [StreamKey]. @@ -66,7 +66,7 @@ type Actor struct { type NewActorParams struct { APIPort int // defaults to 9997 RTMPAddr domain.NetAddr // defaults to 127.0.0.1:1935 - RTMPHost string // defaults to "localhost" + Host string // defaults to "localhost" StreamKey StreamKey // defaults to "live" ChanSize int // defaults to 64 UpdateStateInterval time.Duration // defaults to 5 seconds @@ -95,7 +95,7 @@ func NewActor(ctx context.Context, params NewActorParams) (_ *Actor, err error) return &Actor{ apiPort: cmp.Or(params.APIPort, defaultAPIPort), rtmpAddr: rtmpAddr, - rtmpHost: cmp.Or(params.RTMPHost, defaultRTMPHost), + rtmpHost: cmp.Or(params.Host, defaultHost), streamKey: cmp.Or(params.StreamKey, defaultStreamKey), updateStateInterval: cmp.Or(params.UpdateStateInterval, defaultUpdateStateInterval), tlsCert: tlsCert,