487 lines
13 KiB
Go
487 lines
13 KiB
Go
// 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
|
|
}
|