// Package conf contains the struct that holds the configuration of the software. package conf import ( "encoding/base64" "encoding/json" "fmt" "os" "reflect" "sort" "strings" "time" "github.com/bluenviron/gohlslib" "github.com/bluenviron/gortsplib/v3" "github.com/bluenviron/gortsplib/v3/pkg/headers" "golang.org/x/crypto/nacl/secretbox" "gopkg.in/yaml.v2" "github.com/aler9/mediamtx/internal/logger" ) func decrypt(key string, byts []byte) ([]byte, error) { enc, err := base64.StdEncoding.DecodeString(string(byts)) if err != nil { return nil, err } var secretKey [32]byte copy(secretKey[:], key) var decryptNonce [24]byte copy(decryptNonce[:], enc[:24]) decrypted, ok := secretbox.Open(nil, enc[24:], &decryptNonce, &secretKey) if !ok { return nil, fmt.Errorf("decryption error") } return decrypted, nil } func loadFromFile(fpath string, conf *Conf) (bool, error) { if fpath == "mediamtx.yml" { // give priority to the legacy configuration file, in order not to break // existing setups if _, err := os.Stat("rtsp-simple-server.yml"); err == nil { fpath = "rtsp-simple-server.yml" } } // mediamtx.yml is optional // other configuration files are not if fpath == "mediamtx.yml" || fpath == "rtsp-simple-server.yml" { if _, err := os.Stat(fpath); err != nil { return false, nil } } byts, err := os.ReadFile(fpath) if err != nil { return true, err } if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { // legacy format byts, err = decrypt(key, byts) if err != nil { return true, err } } if key, ok := os.LookupEnv("MTX_CONFKEY"); ok { byts, err = decrypt(key, byts) if err != nil { return true, err } } // load YAML config into a generic map var temp interface{} err = yaml.Unmarshal(byts, &temp) if err != nil { return true, err } // convert interface{} keys into string keys to avoid JSON errors var convert func(i interface{}) (interface{}, error) convert = func(i interface{}) (interface{}, error) { switch x := i.(type) { case map[interface{}]interface{}: m2 := map[string]interface{}{} for k, v := range x { ks, ok := k.(string) if !ok { return nil, fmt.Errorf("integer keys are not supported (%v)", k) } m2[ks], err = convert(v) if err != nil { return nil, err } } return m2, nil case []interface{}: a2 := make([]interface{}, len(x)) for i, v := range x { a2[i], err = convert(v) if err != nil { return nil, err } } return a2, nil } return i, nil } temp, err = convert(temp) if err != nil { return false, err } // check for non-existent parameters var checkNonExistentFields func(what interface{}, ref interface{}) error checkNonExistentFields = func(what interface{}, ref interface{}) error { if what == nil { return nil } ma, ok := what.(map[string]interface{}) if !ok { return fmt.Errorf("not a map") } for k, v := range ma { fi := func() reflect.Type { rr := reflect.TypeOf(ref) for i := 0; i < rr.NumField(); i++ { f := rr.Field(i) if f.Tag.Get("json") == k { return f.Type } } return nil }() if fi == nil { return fmt.Errorf("non-existent parameter: '%s'", k) } if fi == reflect.TypeOf(map[string]*PathConf{}) && v != nil { ma2, ok := v.(map[string]interface{}) if !ok { return fmt.Errorf("parameter %s is not a map", k) } for k2, v2 := range ma2 { err := checkNonExistentFields(v2, reflect.Zero(fi.Elem().Elem()).Interface()) if err != nil { return fmt.Errorf("parameter %s, key %s: %s", k, k2, err) } } } } return nil } err = checkNonExistentFields(temp, Conf{}) if err != nil { return true, err } // convert the generic map into JSON byts, err = json.Marshal(temp) if err != nil { return true, err } // load the configuration from JSON err = json.Unmarshal(byts, conf) if err != nil { return true, err } return true, nil } // Conf is a configuration. type Conf struct { // general LogLevel LogLevel `json:"logLevel"` LogDestinations LogDestinations `json:"logDestinations"` LogFile string `json:"logFile"` ReadTimeout StringDuration `json:"readTimeout"` WriteTimeout StringDuration `json:"writeTimeout"` ReadBufferCount int `json:"readBufferCount"` UDPMaxPayloadSize int `json:"udpMaxPayloadSize"` ExternalAuthenticationURL string `json:"externalAuthenticationURL"` API bool `json:"api"` APIAddress string `json:"apiAddress"` Metrics bool `json:"metrics"` MetricsAddress string `json:"metricsAddress"` PPROF bool `json:"pprof"` PPROFAddress string `json:"pprofAddress"` RunOnConnect string `json:"runOnConnect"` RunOnConnectRestart bool `json:"runOnConnectRestart"` // RTSP RTSPDisable bool `json:"rtspDisable"` Protocols Protocols `json:"protocols"` Encryption Encryption `json:"encryption"` RTSPAddress string `json:"rtspAddress"` RTSPSAddress string `json:"rtspsAddress"` RTPAddress string `json:"rtpAddress"` RTCPAddress string `json:"rtcpAddress"` MulticastIPRange string `json:"multicastIPRange"` MulticastRTPPort int `json:"multicastRTPPort"` MulticastRTCPPort int `json:"multicastRTCPPort"` ServerKey string `json:"serverKey"` ServerCert string `json:"serverCert"` AuthMethods AuthMethods `json:"authMethods"` // RTMP RTMPDisable bool `json:"rtmpDisable"` RTMPAddress string `json:"rtmpAddress"` RTMPEncryption Encryption `json:"rtmpEncryption"` RTMPSAddress string `json:"rtmpsAddress"` RTMPServerKey string `json:"rtmpServerKey"` RTMPServerCert string `json:"rtmpServerCert"` // HLS HLSDisable bool `json:"hlsDisable"` HLSAddress string `json:"hlsAddress"` HLSEncryption bool `json:"hlsEncryption"` HLSServerKey string `json:"hlsServerKey"` HLSServerCert string `json:"hlsServerCert"` HLSAlwaysRemux bool `json:"hlsAlwaysRemux"` HLSVariant HLSVariant `json:"hlsVariant"` HLSSegmentCount int `json:"hlsSegmentCount"` HLSSegmentDuration StringDuration `json:"hlsSegmentDuration"` HLSPartDuration StringDuration `json:"hlsPartDuration"` HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"` HLSAllowOrigin string `json:"hlsAllowOrigin"` HLSTrustedProxies IPsOrCIDRs `json:"hlsTrustedProxies"` HLSDirectory string `json:"hlsDirectory"` // WebRTC WebRTCDisable bool `json:"webrtcDisable"` WebRTCAddress string `json:"webrtcAddress"` WebRTCEncryption bool `json:"webrtcEncryption"` WebRTCServerKey string `json:"webrtcServerKey"` WebRTCServerCert string `json:"webrtcServerCert"` WebRTCAllowOrigin string `json:"webrtcAllowOrigin"` WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"` WebRTCICEServers []string `json:"webrtcICEServers"` WebRTCICEHostNAT1To1IPs []string `json:"webrtcICEHostNAT1To1IPs"` WebRTCICEUDPMuxAddress string `json:"webrtcICEUDPMuxAddress"` WebRTCICETCPMuxAddress string `json:"webrtcICETCPMuxAddress"` // paths Paths map[string]*PathConf `json:"paths"` } // Load loads a Conf. func Load(fpath string) (*Conf, bool, error) { conf := &Conf{} found, err := loadFromFile(fpath, conf) if err != nil { return nil, false, err } err = loadFromEnvironment("RTSP", conf) // legacy prefix if err != nil { return nil, false, err } err = loadFromEnvironment("MTX", conf) if err != nil { return nil, false, err } err = conf.CheckAndFillMissing() if err != nil { return nil, false, err } return conf, found, nil } // Clone clones the configuration. func (conf Conf) Clone() *Conf { enc, err := json.Marshal(conf) if err != nil { panic(err) } var dest Conf err = json.Unmarshal(enc, &dest) if err != nil { panic(err) } return &dest } // CheckAndFillMissing checks the configuration for errors and fills missing parameters. func (conf *Conf) CheckAndFillMissing() error { // general if conf.LogLevel == 0 { conf.LogLevel = LogLevel(logger.Info) } if len(conf.LogDestinations) == 0 { conf.LogDestinations = LogDestinations{logger.DestinationStdout: {}} } if conf.LogFile == "" { conf.LogFile = "mediamtx.log" } if conf.ReadTimeout == 0 { conf.ReadTimeout = 10 * StringDuration(time.Second) } if conf.WriteTimeout == 0 { conf.WriteTimeout = 10 * StringDuration(time.Second) } if conf.ReadBufferCount == 0 { conf.ReadBufferCount = 512 } if (conf.ReadBufferCount & (conf.ReadBufferCount - 1)) != 0 { return fmt.Errorf("'readBufferCount' must be a power of two") } if conf.UDPMaxPayloadSize == 0 { conf.UDPMaxPayloadSize = 1472 } if conf.UDPMaxPayloadSize > 1472 { return fmt.Errorf("'udpMaxPayloadSize' must be less than 1472") } if conf.ExternalAuthenticationURL != "" { if !strings.HasPrefix(conf.ExternalAuthenticationURL, "http://") && !strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") { return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL") } } if conf.APIAddress == "" { conf.APIAddress = "127.0.0.1:9997" } if conf.MetricsAddress == "" { conf.MetricsAddress = "127.0.0.1:9998" } if conf.PPROFAddress == "" { conf.PPROFAddress = "127.0.0.1:9999" } // RTSP if len(conf.Protocols) == 0 { conf.Protocols = Protocols{ Protocol(gortsplib.TransportUDP): {}, Protocol(gortsplib.TransportUDPMulticast): {}, Protocol(gortsplib.TransportTCP): {}, } } if conf.Encryption == EncryptionStrict { if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDP)]; ok { return fmt.Errorf("strict encryption can't be used with the UDP transport protocol") } if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDPMulticast)]; ok { return fmt.Errorf("strict encryption can't be used with the UDP-multicast transport protocol") } } if conf.RTSPAddress == "" { conf.RTSPAddress = ":8554" } if conf.RTSPSAddress == "" { conf.RTSPSAddress = ":8322" } if conf.RTPAddress == "" { conf.RTPAddress = ":8000" } if conf.RTCPAddress == "" { conf.RTCPAddress = ":8001" } if conf.MulticastIPRange == "" { conf.MulticastIPRange = "224.1.0.0/16" } if conf.MulticastRTPPort == 0 { conf.MulticastRTPPort = 8002 } if conf.MulticastRTCPPort == 0 { conf.MulticastRTCPPort = 8003 } if conf.ServerKey == "" { conf.ServerKey = "server.key" } if conf.ServerCert == "" { conf.ServerCert = "server.crt" } if len(conf.AuthMethods) == 0 { conf.AuthMethods = AuthMethods{headers.AuthBasic, headers.AuthDigest} } // RTMP if conf.RTMPAddress == "" { conf.RTMPAddress = ":1935" } if conf.RTMPSAddress == "" { conf.RTMPSAddress = ":1936" } // HLS if conf.HLSAddress == "" { conf.HLSAddress = ":8888" } if conf.HLSServerKey == "" { conf.HLSServerKey = "server.key" } if conf.HLSServerCert == "" { conf.HLSServerCert = "server.crt" } if conf.HLSVariant == 0 { conf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency) } if conf.HLSSegmentCount == 0 { conf.HLSSegmentCount = 7 } if conf.HLSSegmentDuration == 0 { conf.HLSSegmentDuration = 1 * StringDuration(time.Second) } if conf.HLSPartDuration == 0 { conf.HLSPartDuration = 200 * StringDuration(time.Millisecond) } if conf.HLSSegmentMaxSize == 0 { conf.HLSSegmentMaxSize = 50 * 1024 * 1024 } if conf.HLSAllowOrigin == "" { conf.HLSAllowOrigin = "*" } // WebRTC if conf.WebRTCAddress == "" { conf.WebRTCAddress = ":8889" } if conf.WebRTCServerKey == "" { conf.WebRTCServerKey = "server.key" } if conf.WebRTCServerCert == "" { conf.WebRTCServerCert = "server.crt" } if conf.WebRTCAllowOrigin == "" { conf.WebRTCAllowOrigin = "*" } if conf.WebRTCICEServers == nil { conf.WebRTCICEServers = []string{"stun:stun.l.google.com:19302"} } // do not add automatically "all", since user may want to // initialize all paths through API or hot reloading. if conf.Paths == nil { conf.Paths = make(map[string]*PathConf) } // "all" is an alias for "~^.*$" if _, ok := conf.Paths["all"]; ok { conf.Paths["~^.*$"] = conf.Paths["all"] delete(conf.Paths, "all") } sortedNames := make([]string, len(conf.Paths)) i := 0 for name := range conf.Paths { sortedNames[i] = name i++ } sort.Strings(sortedNames) for _, name := range sortedNames { pconf := conf.Paths[name] if pconf == nil { pconf = &PathConf{} conf.Paths[name] = pconf } err := pconf.checkAndFillMissing(conf, name) if err != nil { return err } } return nil }