refactor(config)!: update config file schema

BREAKING CHANGE: YAML schema
This commit is contained in:
Rob Watson 2025-04-18 11:33:13 +02:00
parent d35dedb15b
commit 5f026be769
8 changed files with 52 additions and 44 deletions

View File

@ -97,11 +97,10 @@ logfile:
enabled: true # defaults to false enabled: true # defaults to false
path: /path/to/logfile # defaults to $XDG_STATE_HOME/octoplex/octoplex.log path: /path/to/logfile # defaults to $XDG_STATE_HOME/octoplex/octoplex.log
sources: sources:
rtmp: mediaServer:
enabled: true # must be true
streamKey: live # defaults to "live" streamKey: live # defaults to "live"
host: rtmp.example.com # defaults to "localhost" 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 ip: 0.0.0.0 # defaults to 127.0.0.1
port: 1935 # defaults to 1935 port: 1935 # defaults to 1935
destinations: destinations:
@ -115,8 +114,8 @@ destinations:
:information_source: It is also possible to add and remove destinations directly from the :information_source: It is also possible to add and remove destinations directly from the
terminal user interface. terminal user interface.
:warning: `sources.rtmp.bindAddr.ip` must be set to a valid IP address if you want :warning: `sources.mediaServer.rtmp.ip` must be set to a valid IP address if
to accept connections from other hosts. Leave it blank to bind only to 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. localhost (`127.0.0.1`) or use `0.0.0.0` to bind to all network interfaces.
## Contributing ## Contributing

View File

@ -39,8 +39,8 @@ func Run(ctx context.Context, params RunParams) error {
applyConfig(cfg, state) applyConfig(cfg, state)
// While RTMP is the only source, it doesn't make sense to disable it. // While RTMP is the only source, it doesn't make sense to disable it.
if !cfg.Sources.RTMP.Enabled { if cfg.Sources.MediaServer.RTMP == nil {
return errors.New("config: sources.rtmp.enabled must be set to true") return errors.New("config: sources.mediaServer.rtmp is required")
} }
logger := params.Logger logger := params.Logger
@ -89,9 +89,9 @@ func Run(ctx context.Context, params RunParams) error {
updateUI() updateUI()
srv, err := mediaserver.NewActor(ctx, mediaserver.NewActorParams{ srv, err := mediaserver.NewActor(ctx, mediaserver.NewActorParams{
RTMPAddr: domain.NetAddr(cfg.Sources.RTMP.BindAddr), RTMPAddr: domain.NetAddr(cfg.Sources.MediaServer.RTMP.NetAddr),
RTMPHost: cfg.Sources.RTMP.Host, Host: cfg.Sources.MediaServer.Host,
StreamKey: mediaserver.StreamKey(cfg.Sources.RTMP.StreamKey), StreamKey: mediaserver.StreamKey(cfg.Sources.MediaServer.StreamKey),
ContainerClient: containerClient, ContainerClient: containerClient,
Logger: logger.With("component", "mediaserver"), Logger: logger.With("component", "mediaserver"),
}) })

View File

@ -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) destURL2 := fmt.Sprintf("rtmp://%s:%d/%s/dest2", hostIP, destServerPort.Int(), wantStreamKey)
configService := setupConfigService(t, config.Config{ configService := setupConfigService(t, config.Config{
Sources: config.Sources{ Sources: config.Sources{
RTMP: config.RTMPSource{ MediaServer: config.MediaServerSource{
Enabled: true,
Host: rtmpHost, Host: rtmpHost,
BindAddr: config.NetAddr{IP: rtmpIP, Port: rtmpPort},
StreamKey: streamKey, StreamKey: streamKey,
}}, RTMP: &config.RTMPSource{
NetAddr: config.NetAddr{IP: rtmpIP, Port: rtmpPort},
}},
},
// Load one destination from config, add the other in-app. // Load one destination from config, add the other in-app.
Destinations: []config.Destination{{Name: "Local server 1", URL: destURL1}}, Destinations: []config.Destination{{Name: "Local server 1", URL: destURL1}},
}) })
@ -275,9 +276,9 @@ func TestIntegrationCustomRTMPURL(t *testing.T) {
configService := setupConfigService(t, config.Config{ configService := setupConfigService(t, config.Config{
Sources: config.Sources{ Sources: config.Sources{
RTMP: config.RTMPSource{ MediaServer: config.MediaServerSource{
Enabled: true, Host: "rtmp.live.tv",
Host: "rtmp.live.tv", RTMP: &config.RTMPSource{},
}, },
}, },
}) })
@ -336,7 +337,7 @@ func TestIntegrationRestartDestination(t *testing.T) {
screen, screenCaptureC, getContents := setupSimulationScreen(t) screen, screenCaptureC, getContents := setupSimulationScreen(t)
configService := setupConfigService(t, config.Config{ 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{{ Destinations: []config.Destination{{
Name: "Local server 1", Name: "Local server 1",
URL: fmt.Sprintf("rtmp://%s:%d/live", hostIP, destServerRTMPPort.Int()), URL: fmt.Sprintf("rtmp://%s:%d/live", hostIP, destServerRTMPPort.Int()),
@ -482,7 +483,7 @@ func TestIntegrationStartDestinationFailed(t *testing.T) {
screen, screenCaptureC, getContents := setupSimulationScreen(t) screen, screenCaptureC, getContents := setupSimulationScreen(t)
configService := setupConfigService(t, config.Config{ 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"}}, 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) screen, screenCaptureC, getContents := setupSimulationScreen(t)
configService := setupConfigService(t, config.Config{ 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{}) done := make(chan struct{})
@ -701,7 +702,7 @@ func TestIntegrationStartupCheck(t *testing.T) {
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
require.NoError(t, err) 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) screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{}) done := make(chan struct{})
@ -770,7 +771,7 @@ func TestIntegrationMediaServerError(t *testing.T) {
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
require.NoError(t, err) 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) screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{}) done := make(chan struct{})
@ -809,7 +810,7 @@ func TestIntegrationDockerClientError(t *testing.T) {
var dockerClient mocks.DockerClient var dockerClient mocks.DockerClient
dockerClient.EXPECT().NetworkCreate(mock.Anything, mock.Anything, mock.Anything).Return(network.CreateResponse{}, errors.New("boom")) 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) screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{}) done := make(chan struct{})
@ -850,7 +851,7 @@ func TestIntegrationDockerConnectionError(t *testing.T) {
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.WithHost("http://docker.example.com")) dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.WithHost("http://docker.example.com"))
require.NoError(t, err) 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) screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{}) done := make(chan struct{})

View File

@ -30,15 +30,19 @@ type NetAddr struct {
// RTMPSource holds the configuration for the RTMP source. // RTMPSource holds the configuration for the RTMP source.
type RTMPSource struct { type RTMPSource struct {
Enabled bool `yaml:"enabled"` NetAddr `yaml:",inline"`
StreamKey string `yaml:"streamKey,omitempty"` }
Host string `yaml:"host,omitempty"`
BindAddr NetAddr `yaml:"bindAddr,omitempty"` // 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. // Sources holds the configuration for the sources.
type Sources struct { type Sources struct {
RTMP RTMPSource `yaml:"rtmp"` MediaServer MediaServerSource `yaml:"mediaServer"`
} }
// Config holds the configuration for the application. // Config holds the configuration for the application.

View File

@ -182,8 +182,8 @@ func (s *Service) writeConfig(cfgBytes []byte) error {
// //
// This function may set exported fields to arbitrary values. // This function may set exported fields to arbitrary values.
func (s *Service) populateConfigOnBuild(cfg *Config) { func (s *Service) populateConfigOnBuild(cfg *Config) {
cfg.Sources.RTMP.Enabled = true cfg.Sources.MediaServer.StreamKey = "live"
cfg.Sources.RTMP.StreamKey = "live" cfg.Sources.MediaServer.RTMP = &RTMPSource{NetAddr{"127.0.0.1", 1935}}
s.populateConfigOnRead(cfg) s.populateConfigOnRead(cfg)
} }

View File

@ -44,7 +44,9 @@ func TestConfigServiceCurrent(t *testing.T) {
t.Cleanup(func() { require.NoError(t, os.RemoveAll(systemConfigDir)) }) t.Cleanup(func() { require.NoError(t, os.RemoveAll(systemConfigDir)) })
// Ensure defaults are set: // 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) { func TestConfigServiceCreateConfig(t *testing.T) {
@ -67,7 +69,9 @@ func TestConfigServiceCreateConfig(t *testing.T) {
var readCfg config.Config var readCfg config.Config
require.NoError(t, yaml.Unmarshal(cfgBytes, &readCfg)) 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) { func TestConfigServiceReadConfig(t *testing.T) {
@ -90,13 +94,14 @@ func TestConfigServiceReadConfig(t *testing.T) {
Path: "test.log", Path: "test.log",
}, },
Sources: config.Sources{ Sources: config.Sources{
RTMP: config.RTMPSource{ MediaServer: config.MediaServerSource{
Enabled: true,
StreamKey: "s3cr3t", StreamKey: "s3cr3t",
Host: "rtmp.example.com", Host: "rtmp.example.com",
BindAddr: config.NetAddr{ RTMP: &config.RTMPSource{
IP: "0.0.0.0", NetAddr: config.NetAddr{
Port: 19350, IP: "0.0.0.0",
Port: 19350,
},
}, },
}, },
}, },

View File

@ -3,11 +3,10 @@ logfile:
enabled: true enabled: true
path: test.log path: test.log
sources: sources:
rtmp: mediaServer:
enabled: true
streamKey: s3cr3t streamKey: s3cr3t
host: rtmp.example.com host: rtmp.example.com
bindAddr: rtmp:
ip: 0.0.0.0 ip: 0.0.0.0
port: 19350 port: 19350
destinations: destinations:

View File

@ -30,7 +30,7 @@ const (
defaultAPIPort = 9997 // default API host port for the media server defaultAPIPort = 9997 // default API host port for the media server
defaultRTMPIP = "127.0.0.1" // default RTMP host IP, bound to localhost for security defaultRTMPIP = "127.0.0.1" // default RTMP host IP, bound to localhost for security
defaultRTMPPort = 1935 // default RTMP host port for the media server 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 defaultChanSize = 64 // default channel size for asynchronous non-error channels
imageNameMediaMTX = "ghcr.io/rfwatson/mediamtx-alpine:latest" // image name for mediamtx imageNameMediaMTX = "ghcr.io/rfwatson/mediamtx-alpine:latest" // image name for mediamtx
defaultStreamKey StreamKey = "live" // Default stream key. See [StreamKey]. defaultStreamKey StreamKey = "live" // Default stream key. See [StreamKey].
@ -66,7 +66,7 @@ type Actor struct {
type NewActorParams struct { type NewActorParams struct {
APIPort int // defaults to 9997 APIPort int // defaults to 9997
RTMPAddr domain.NetAddr // defaults to 127.0.0.1:1935 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" StreamKey StreamKey // defaults to "live"
ChanSize int // defaults to 64 ChanSize int // defaults to 64
UpdateStateInterval time.Duration // defaults to 5 seconds UpdateStateInterval time.Duration // defaults to 5 seconds
@ -95,7 +95,7 @@ func NewActor(ctx context.Context, params NewActorParams) (_ *Actor, err error)
return &Actor{ return &Actor{
apiPort: cmp.Or(params.APIPort, defaultAPIPort), apiPort: cmp.Or(params.APIPort, defaultAPIPort),
rtmpAddr: rtmpAddr, rtmpAddr: rtmpAddr,
rtmpHost: cmp.Or(params.RTMPHost, defaultRTMPHost), rtmpHost: cmp.Or(params.Host, defaultHost),
streamKey: cmp.Or(params.StreamKey, defaultStreamKey), streamKey: cmp.Or(params.StreamKey, defaultStreamKey),
updateStateInterval: cmp.Or(params.UpdateStateInterval, defaultUpdateStateInterval), updateStateInterval: cmp.Or(params.UpdateStateInterval, defaultUpdateStateInterval),
tlsCert: tlsCert, tlsCert: tlsCert,