Browse Source

allow setting rpiCameraSaturation to 0 (#1651) (#1772)

pull/1774/head
Alessandro Ros 2 years ago committed by GitHub
parent
commit
e998688757
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 6
      internal/conf/authmethod.go
  3. 341
      internal/conf/conf.go
  4. 37
      internal/conf/conf_test.go
  5. 4
      internal/conf/credential.go
  6. 29
      internal/conf/decrypt/decrypt.go
  7. 4
      internal/conf/encryption.go
  8. 14
      internal/conf/env/env.go
  9. 33
      internal/conf/env/env_test.go
  10. 4
      internal/conf/hlsvariant.go
  11. 6
      internal/conf/ipsorcidrs.go
  12. 6
      internal/conf/logdestination.go
  13. 4
      internal/conf/loglevel.go
  14. 85
      internal/conf/path.go
  15. 4
      internal/conf/protocol.go
  16. 4
      internal/conf/sourceprotocol.go
  17. 4
      internal/conf/stringduration.go
  18. 4
      internal/conf/stringsize.go
  19. 70
      internal/conf/yaml/load.go
  20. 8
      internal/core/api.go

2
README.md

@ -1202,7 +1202,7 @@ For more advanced options, you can create and serve a custom web page by startin
* [RTSP/RTP/RTCP standards](https://github.com/bluenviron/gortsplib#standards) * [RTSP/RTP/RTCP standards](https://github.com/bluenviron/gortsplib#standards)
* [HLS standards](https://github.com/bluenviron/gohlslib#standards) * [HLS standards](https://github.com/bluenviron/gohlslib#standards)
* [Codec standards](https://github.com/bluenviron/mediacommon#standards) * [Codec standards](https://github.com/bluenviron/mediacommon#standards)
* [RTMP specification](https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf) * [RTMP](https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf)
* [Enhanced RTMP](https://raw.githubusercontent.com/veovera/enhanced-rtmp/main/enhanced-rtmp-v1.pdf) * [Enhanced RTMP](https://raw.githubusercontent.com/veovera/enhanced-rtmp/main/enhanced-rtmp-v1.pdf)
* [Golang project layout](https://github.com/golang-standards/project-layout) * [Golang project layout](https://github.com/golang-standards/project-layout)

6
internal/conf/authmethod.go

@ -41,6 +41,8 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error {
return err return err
} }
*d = nil
for _, v := range in { for _, v := range in {
switch v { switch v {
case "basic": case "basic":
@ -57,8 +59,8 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *AuthMethods) unmarshalEnv(s string) error { func (d *AuthMethods) UnmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ",")) byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts) return d.UnmarshalJSON(byts)
} }

341
internal/conf/conf.go

@ -2,11 +2,10 @@
package conf package conf
import ( import (
"encoding/base64" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"reflect"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -14,29 +13,22 @@ import (
"github.com/bluenviron/gohlslib" "github.com/bluenviron/gohlslib"
"github.com/bluenviron/gortsplib/v3" "github.com/bluenviron/gortsplib/v3"
"github.com/bluenviron/gortsplib/v3/pkg/headers" "github.com/bluenviron/gortsplib/v3/pkg/headers"
"golang.org/x/crypto/nacl/secretbox"
"gopkg.in/yaml.v2"
"github.com/aler9/mediamtx/internal/conf/decrypt"
"github.com/aler9/mediamtx/internal/conf/env"
"github.com/aler9/mediamtx/internal/conf/yaml"
"github.com/aler9/mediamtx/internal/logger" "github.com/aler9/mediamtx/internal/logger"
) )
func decrypt(key string, byts []byte) ([]byte, error) { func getSortedKeys(paths map[string]*PathConf) []string {
enc, err := base64.StdEncoding.DecodeString(string(byts)) ret := make([]string, len(paths))
if err != nil { i := 0
return nil, err for name := range paths {
} ret[i] = name
i++
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")
} }
sort.Strings(ret)
return decrypted, nil return ret
} }
func loadFromFile(fpath string, conf *Conf) (bool, error) { func loadFromFile(fpath string, conf *Conf) (bool, error) {
@ -52,6 +44,7 @@ func loadFromFile(fpath string, conf *Conf) (bool, error) {
// other configuration files are not // other configuration files are not
if fpath == "mediamtx.yml" || fpath == "rtsp-simple-server.yml" { if fpath == "mediamtx.yml" || fpath == "rtsp-simple-server.yml" {
if _, err := os.Stat(fpath); err != nil { if _, err := os.Stat(fpath); err != nil {
conf.UnmarshalJSON(nil) // load defaults
return false, nil return false, nil
} }
} }
@ -62,119 +55,20 @@ func loadFromFile(fpath string, conf *Conf) (bool, error) {
} }
if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { // legacy format if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { // legacy format
byts, err = decrypt(key, byts) byts, err = decrypt.Decrypt(key, byts)
if err != nil { if err != nil {
return true, err return true, err
} }
} }
if key, ok := os.LookupEnv("MTX_CONFKEY"); ok { if key, ok := os.LookupEnv("MTX_CONFKEY"); ok {
byts, err = decrypt(key, byts) byts, err = decrypt.Decrypt(key, byts)
if err != nil { if err != nil {
return true, err return true, err
} }
} }
// load YAML config into a generic map err = yaml.Load(byts, conf)
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 { if err != nil {
return true, err return true, err
} }
@ -267,17 +161,17 @@ func Load(fpath string) (*Conf, bool, error) {
return nil, false, err return nil, false, err
} }
err = loadFromEnvironment("RTSP", conf) // legacy prefix err = env.Load("RTSP", conf) // legacy prefix
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
err = loadFromEnvironment("MTX", conf) err = env.Load("MTX", conf)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
err = conf.CheckAndFillMissing() err = conf.Check()
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
@ -301,33 +195,12 @@ func (conf Conf) Clone() *Conf {
return &dest return &dest
} }
// CheckAndFillMissing checks the configuration for errors and fills missing parameters. // Check checks the configuration for errors.
func (conf *Conf) CheckAndFillMissing() error { func (conf *Conf) Check() error {
// general // 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 { if (conf.ReadBufferCount & (conf.ReadBufferCount - 1)) != 0 {
return fmt.Errorf("'readBufferCount' must be a power of two") return fmt.Errorf("'readBufferCount' must be a power of two")
} }
if conf.UDPMaxPayloadSize == 0 {
conf.UDPMaxPayloadSize = 1472
}
if conf.UDPMaxPayloadSize > 1472 { if conf.UDPMaxPayloadSize > 1472 {
return fmt.Errorf("'udpMaxPayloadSize' must be less than 1472") return fmt.Errorf("'udpMaxPayloadSize' must be less than 1472")
} }
@ -337,24 +210,8 @@ func (conf *Conf) CheckAndFillMissing() error {
return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL") 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 // RTSP
if len(conf.Protocols) == 0 {
conf.Protocols = Protocols{
Protocol(gortsplib.TransportUDP): {},
Protocol(gortsplib.TransportUDPMulticast): {},
Protocol(gortsplib.TransportTCP): {},
}
}
if conf.Encryption == EncryptionStrict { if conf.Encryption == EncryptionStrict {
if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDP)]; ok { if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDP)]; ok {
return fmt.Errorf("strict encryption can't be used with the UDP transport protocol") return fmt.Errorf("strict encryption can't be used with the UDP transport protocol")
@ -364,90 +221,6 @@ func (conf *Conf) CheckAndFillMissing() error {
return fmt.Errorf("strict encryption can't be used with the UDP-multicast transport protocol") 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 // do not add automatically "all", since user may want to
// initialize all paths through API or hot reloading. // initialize all paths through API or hot reloading.
@ -461,22 +234,15 @@ func (conf *Conf) CheckAndFillMissing() error {
delete(conf.Paths, "all") delete(conf.Paths, "all")
} }
sortedNames := make([]string, len(conf.Paths)) for _, name := range getSortedKeys(conf.Paths) {
i := 0
for name := range conf.Paths {
sortedNames[i] = name
i++
}
sort.Strings(sortedNames)
for _, name := range sortedNames {
pconf := conf.Paths[name] pconf := conf.Paths[name]
if pconf == nil { if pconf == nil {
pconf = &PathConf{} pconf = &PathConf{}
pconf.UnmarshalJSON(nil) // fill defaults
conf.Paths[name] = pconf conf.Paths[name] = pconf
} }
err := pconf.checkAndFillMissing(conf, name) err := pconf.check(conf, name)
if err != nil { if err != nil {
return err return err
} }
@ -484,3 +250,62 @@ func (conf *Conf) CheckAndFillMissing() error {
return nil return nil
} }
// UnmarshalJSON implements json.Unmarshaler. It is used to set default values.
func (conf *Conf) UnmarshalJSON(b []byte) error {
// general
conf.LogLevel = LogLevel(logger.Info)
conf.LogDestinations = LogDestinations{logger.DestinationStdout}
conf.LogFile = "mediamtx.log"
conf.ReadTimeout = 10 * StringDuration(time.Second)
conf.WriteTimeout = 10 * StringDuration(time.Second)
conf.ReadBufferCount = 512
conf.UDPMaxPayloadSize = 1472
conf.APIAddress = "127.0.0.1:9997"
conf.MetricsAddress = "127.0.0.1:9998"
conf.PPROFAddress = "127.0.0.1:9999"
// RTSP
conf.Protocols = Protocols{
Protocol(gortsplib.TransportUDP): {},
Protocol(gortsplib.TransportUDPMulticast): {},
Protocol(gortsplib.TransportTCP): {},
}
conf.RTSPAddress = ":8554"
conf.RTSPSAddress = ":8322"
conf.RTPAddress = ":8000"
conf.RTCPAddress = ":8001"
conf.MulticastIPRange = "224.1.0.0/16"
conf.MulticastRTPPort = 8002
conf.MulticastRTCPPort = 8003
conf.ServerKey = "server.key"
conf.ServerCert = "server.crt"
conf.AuthMethods = AuthMethods{headers.AuthBasic, headers.AuthDigest}
// RTMP
conf.RTMPAddress = ":1935"
conf.RTMPSAddress = ":1936"
// HLS
conf.HLSAddress = ":8888"
conf.HLSServerKey = "server.key"
conf.HLSServerCert = "server.crt"
conf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency)
conf.HLSSegmentCount = 7
conf.HLSSegmentDuration = 1 * StringDuration(time.Second)
conf.HLSPartDuration = 200 * StringDuration(time.Millisecond)
conf.HLSSegmentMaxSize = 50 * 1024 * 1024
conf.HLSAllowOrigin = "*"
// WebRTC
conf.WebRTCAddress = ":8889"
conf.WebRTCServerKey = "server.key"
conf.WebRTCServerCert = "server.crt"
conf.WebRTCAllowOrigin = "*"
conf.WebRTCICEServers = []string{"stun:stun.l.google.com:19302"}
type alias Conf
d := json.NewDecoder(bytes.NewReader(b))
d.DisallowUnknownFields()
return d.Decode((*alias)(conf))
}

37
internal/conf/conf_test.go

@ -51,6 +51,17 @@ func TestConfFromFile(t *testing.T) {
Source: "publisher", Source: "publisher",
SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
RPICameraWidth: 1920,
RPICameraHeight: 1080,
RPICameraContrast: 1,
RPICameraSaturation: 1,
RPICameraSharpness: 1,
RPICameraFPS: 30,
RPICameraIDRPeriod: 60,
RPICameraBitrate: 1000000,
RPICameraProfile: "main",
RPICameraLevel: "4.1",
RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX",
RunOnDemandStartTimeout: 5 * StringDuration(time.Second), RunOnDemandStartTimeout: 5 * StringDuration(time.Second),
RunOnDemandCloseAfter: 10 * StringDuration(time.Second), RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
}, pa) }, pa)
@ -109,6 +120,17 @@ func TestConfFromFileAndEnv(t *testing.T) {
Source: "rtsp://testing", Source: "rtsp://testing",
SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
RPICameraWidth: 1920,
RPICameraHeight: 1080,
RPICameraContrast: 1,
RPICameraSaturation: 1,
RPICameraSharpness: 1,
RPICameraFPS: 30,
RPICameraIDRPeriod: 60,
RPICameraBitrate: 1000000,
RPICameraProfile: "main",
RPICameraLevel: "4.1",
RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX",
RunOnDemandStartTimeout: 10 * StringDuration(time.Second), RunOnDemandStartTimeout: 10 * StringDuration(time.Second),
RunOnDemandCloseAfter: 10 * StringDuration(time.Second), RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
}, pa) }, pa)
@ -128,6 +150,17 @@ func TestConfFromEnvOnly(t *testing.T) {
Source: "rtsp://testing", Source: "rtsp://testing",
SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
RPICameraWidth: 1920,
RPICameraHeight: 1080,
RPICameraContrast: 1,
RPICameraSaturation: 1,
RPICameraSharpness: 1,
RPICameraFPS: 30,
RPICameraIDRPeriod: 60,
RPICameraBitrate: 1000000,
RPICameraProfile: "main",
RPICameraLevel: "4.1",
RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX",
RunOnDemandStartTimeout: 10 * StringDuration(time.Second), RunOnDemandStartTimeout: 10 * StringDuration(time.Second),
RunOnDemandCloseAfter: 10 * StringDuration(time.Second), RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
}, pa) }, pa)
@ -179,14 +212,14 @@ func TestConfErrors(t *testing.T) {
{ {
"non existent parameter 1", "non existent parameter 1",
`invalid: param`, `invalid: param`,
"non-existent parameter: 'invalid'", "json: unknown field \"invalid\"",
}, },
{ {
"non existent parameter 2", "non existent parameter 2",
"paths:\n" + "paths:\n" +
" mypath:\n" + " mypath:\n" +
" invalid: parameter\n", " invalid: parameter\n",
"parameter paths, key mypath: non-existent parameter: 'invalid'", "json: unknown field \"invalid\"",
}, },
{ {
"invalid path name", "invalid path name",

4
internal/conf/credential.go

@ -36,7 +36,7 @@ func (d *Credential) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *Credential) unmarshalEnv(s string) error { func (d *Credential) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

29
internal/conf/decrypt/decrypt.go

@ -0,0 +1,29 @@
// Package decrypt contains the Decrypt function.
package decrypt
import (
"encoding/base64"
"fmt"
"golang.org/x/crypto/nacl/secretbox"
)
// Decrypt decrypts the configuration with the given key.
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
}

4
internal/conf/encryption.go

@ -60,7 +60,7 @@ func (d *Encryption) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *Encryption) unmarshalEnv(s string) error { func (d *Encryption) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

14
internal/conf/env.go → internal/conf/env/env.go vendored

@ -1,6 +1,8 @@
package conf // Package env contains a function to load configuration from environment.
package env
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
@ -9,7 +11,7 @@ import (
) )
type envUnmarshaler interface { type envUnmarshaler interface {
unmarshalEnv(string) error UnmarshalEnv(string) error
} }
func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) error { func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) error {
@ -17,7 +19,7 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
if i, ok := rv.Addr().Interface().(envUnmarshaler); ok { if i, ok := rv.Addr().Interface().(envUnmarshaler); ok {
if ev, ok := env[prefix]; ok { if ev, ok := env[prefix]; ok {
err := i.unmarshalEnv(ev) err := i.UnmarshalEnv(ev)
if err != nil { if err != nil {
return fmt.Errorf("%s: %s", prefix, err) return fmt.Errorf("%s: %s", prefix, err)
} }
@ -105,6 +107,9 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
zero := reflect.Value{} zero := reflect.Value{}
if nv == zero { if nv == zero {
nv = reflect.New(rt.Elem().Elem()) nv = reflect.New(rt.Elem().Elem())
if unm, ok := nv.Interface().(json.Unmarshaler); ok {
unm.UnmarshalJSON(nil) // load defaults
}
rv.SetMapIndex(reflect.ValueOf(mapKeyLower), nv) rv.SetMapIndex(reflect.ValueOf(mapKeyLower), nv)
} }
@ -148,7 +153,8 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
return fmt.Errorf("unsupported type: %v", rt) return fmt.Errorf("unsupported type: %v", rt)
} }
func loadFromEnvironment(prefix string, v interface{}) error { // Load loads the configuration from the environment.
func Load(prefix string, v interface{}) error {
env := make(map[string]string) env := make(map[string]string)
for _, kv := range os.Environ() { for _, kv := range os.Environ() {
tmp := strings.SplitN(kv, "=", 2) tmp := strings.SplitN(kv, "=", 2)

33
internal/conf/env_test.go → internal/conf/env/env_test.go vendored

@ -1,6 +1,7 @@
package conf package env
import ( import (
"encoding/json"
"os" "os"
"testing" "testing"
"time" "time"
@ -17,18 +18,40 @@ type mapEntry struct {
MyStruct subStruct MyStruct subStruct
} }
type myDuration time.Duration
func (d *myDuration) UnmarshalJSON(b []byte) error {
var in string
if err := json.Unmarshal(b, &in); err != nil {
return err
}
du, err := time.ParseDuration(in)
if err != nil {
return err
}
*d = myDuration(du)
return nil
}
// UnmarshalEnv implements envUnmarshaler.
func (d *myDuration) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`))
}
type testStruct struct { type testStruct struct {
MyString string MyString string
MyInt int MyInt int
MyFloat float64 MyFloat float64
MyBool bool MyBool bool
MyDuration StringDuration MyDuration myDuration
MyMap map[string]*mapEntry MyMap map[string]*mapEntry
MySlice []string MySlice []string
MySliceEmpty []string MySliceEmpty []string
} }
func TestEnvironment(t *testing.T) { func TestLoad(t *testing.T) {
os.Setenv("MYPREFIX_MYSTRING", "testcontent") os.Setenv("MYPREFIX_MYSTRING", "testcontent")
defer os.Unsetenv("MYPREFIX_MYSTRING") defer os.Unsetenv("MYPREFIX_MYSTRING")
@ -60,14 +83,14 @@ func TestEnvironment(t *testing.T) {
defer os.Unsetenv("MYPREFIX_MYSLICEEMPTY") defer os.Unsetenv("MYPREFIX_MYSLICEEMPTY")
var s testStruct var s testStruct
err := loadFromEnvironment("MYPREFIX", &s) err := Load("MYPREFIX", &s)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "testcontent", s.MyString) require.Equal(t, "testcontent", s.MyString)
require.Equal(t, 123, s.MyInt) require.Equal(t, 123, s.MyInt)
require.Equal(t, 15.2, s.MyFloat) require.Equal(t, 15.2, s.MyFloat)
require.Equal(t, true, s.MyBool) require.Equal(t, true, s.MyBool)
require.Equal(t, 22*StringDuration(time.Second), s.MyDuration) require.Equal(t, 22*myDuration(time.Second), s.MyDuration)
_, ok := s.MyMap["mykey"] _, ok := s.MyMap["mykey"]
require.Equal(t, true, ok) require.Equal(t, true, ok)

4
internal/conf/hlsvariant.go

@ -55,7 +55,7 @@ func (d *HLSVariant) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *HLSVariant) unmarshalEnv(s string) error { func (d *HLSVariant) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

6
internal/conf/ipsorcidrs.go

@ -31,6 +31,8 @@ func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error {
return err return err
} }
*d = nil
if len(in) == 0 { if len(in) == 0 {
return nil return nil
} }
@ -48,8 +50,8 @@ func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *IPsOrCIDRs) unmarshalEnv(s string) error { func (d *IPsOrCIDRs) UnmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ",")) byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts) return d.UnmarshalJSON(byts)
} }

6
internal/conf/logdestination.go

@ -56,6 +56,8 @@ func (d *LogDestinations) UnmarshalJSON(b []byte) error {
return err return err
} }
*d = nil
for _, dest := range in { for _, dest := range in {
var v logger.Destination var v logger.Destination
switch dest { switch dest {
@ -82,8 +84,8 @@ func (d *LogDestinations) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *LogDestinations) unmarshalEnv(s string) error { func (d *LogDestinations) UnmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ",")) byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts) return d.UnmarshalJSON(byts)
} }

4
internal/conf/loglevel.go

@ -61,7 +61,7 @@ func (d *LogLevel) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *LogLevel) unmarshalEnv(s string) error { func (d *LogLevel) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

85
internal/conf/path.go

@ -1,6 +1,7 @@
package conf package conf
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
@ -104,7 +105,7 @@ type PathConf struct {
RunOnReadRestart bool `json:"runOnReadRestart"` RunOnReadRestart bool `json:"runOnReadRestart"`
} }
func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error { func (pconf *PathConf) check(conf *Conf, name string) error {
// normal path // normal path
if name == "" || name[0] != '~' { if name == "" || name[0] != '~' {
err := IsValidPathName(name) err := IsValidPathName(name)
@ -121,10 +122,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
pconf.Regexp = pathRegexp pconf.Regexp = pathRegexp
} }
if pconf.Source == "" {
pconf.Source = "publisher"
}
switch { switch {
case pconf.Source == "publisher": case pconf.Source == "publisher":
@ -221,40 +218,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
} }
} }
if pconf.RPICameraWidth == 0 {
pconf.RPICameraWidth = 1920
}
if pconf.RPICameraHeight == 0 {
pconf.RPICameraHeight = 1080
}
if pconf.RPICameraContrast == 0 {
pconf.RPICameraContrast = 1
}
if pconf.RPICameraSaturation == 0 {
pconf.RPICameraSaturation = 1
}
if pconf.RPICameraSharpness == 0 {
pconf.RPICameraSharpness = 1
}
if pconf.RPICameraFPS == 0 {
pconf.RPICameraFPS = 30
}
if pconf.RPICameraIDRPeriod == 0 {
pconf.RPICameraIDRPeriod = 60
}
if pconf.RPICameraBitrate == 0 {
pconf.RPICameraBitrate = 1000000
}
if pconf.RPICameraProfile == "" {
pconf.RPICameraProfile = "main"
}
if pconf.RPICameraLevel == "" {
pconf.RPICameraLevel = "4.1"
}
if pconf.RPICameraTextOverlay == "" {
pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX"
}
default: default:
return fmt.Errorf("invalid source: '%s'", pconf.Source) return fmt.Errorf("invalid source: '%s'", pconf.Source)
} }
@ -265,14 +228,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
} }
} }
if pconf.SourceOnDemandStartTimeout == 0 {
pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second)
}
if pconf.SourceOnDemandCloseAfter == 0 {
pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second)
}
if pconf.Fallback != "" { if pconf.Fallback != "" {
if strings.HasPrefix(pconf.Fallback, "/") { if strings.HasPrefix(pconf.Fallback, "/") {
err := IsValidPathName(pconf.Fallback[1:]) err := IsValidPathName(pconf.Fallback[1:])
@ -331,14 +286,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
return fmt.Errorf("'runOnDemand' can be used only when source is 'publisher'") return fmt.Errorf("'runOnDemand' can be used only when source is 'publisher'")
} }
if pconf.RunOnDemandStartTimeout == 0 {
pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second)
}
if pconf.RunOnDemandCloseAfter == 0 {
pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second)
}
return nil return nil
} }
@ -384,3 +331,31 @@ func (pconf PathConf) HasOnDemandStaticSource() bool {
func (pconf PathConf) HasOnDemandPublisher() bool { func (pconf PathConf) HasOnDemandPublisher() bool {
return pconf.RunOnDemand != "" return pconf.RunOnDemand != ""
} }
// UnmarshalJSON implements json.Unmarshaler. It is used to set default values.
func (pconf *PathConf) UnmarshalJSON(b []byte) error {
// source
pconf.Source = "publisher"
pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second)
pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second)
pconf.RPICameraWidth = 1920
pconf.RPICameraHeight = 1080
pconf.RPICameraContrast = 1
pconf.RPICameraSaturation = 1
pconf.RPICameraSharpness = 1
pconf.RPICameraFPS = 30
pconf.RPICameraIDRPeriod = 60
pconf.RPICameraBitrate = 1000000
pconf.RPICameraProfile = "main"
pconf.RPICameraLevel = "4.1"
pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX"
// external commands
pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second)
pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second)
type alias PathConf
d := json.NewDecoder(bytes.NewReader(b))
d.DisallowUnknownFields()
return d.Decode((*alias)(pconf))
}

4
internal/conf/protocol.go

@ -74,8 +74,8 @@ func (d *Protocols) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *Protocols) unmarshalEnv(s string) error { func (d *Protocols) UnmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ",")) byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts) return d.UnmarshalJSON(byts)
} }

4
internal/conf/sourceprotocol.go

@ -67,7 +67,7 @@ func (d *SourceProtocol) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *SourceProtocol) unmarshalEnv(s string) error { func (d *SourceProtocol) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

4
internal/conf/stringduration.go

@ -30,7 +30,7 @@ func (d *StringDuration) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (d *StringDuration) unmarshalEnv(s string) error { func (d *StringDuration) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }

4
internal/conf/stringsize.go

@ -30,7 +30,7 @@ func (s *StringSize) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// unmarshalEnv implements envUnmarshaler. // UnmarshalEnv implements envUnmarshaler.
func (s *StringSize) unmarshalEnv(v string) error { func (s *StringSize) UnmarshalEnv(v string) error {
return s.UnmarshalJSON([]byte(`"` + v + `"`)) return s.UnmarshalJSON([]byte(`"` + v + `"`))
} }

70
internal/conf/yaml/load.go

@ -0,0 +1,70 @@
// Package yaml contains a yaml loader.
package yaml
import (
"bytes"
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
)
func convertKeys(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)
}
var err error
m2[ks], err = convertKeys(v)
if err != nil {
return nil, err
}
}
return m2, nil
case []interface{}:
a2 := make([]interface{}, len(x))
for i, v := range x {
var err error
a2[i], err = convertKeys(v)
if err != nil {
return nil, err
}
}
return a2, nil
}
return i, nil
}
// Load loads the configuration from Yaml.
func Load(buf []byte, dest interface{}) error {
// load YAML into a generic map
var temp interface{}
err := yaml.Unmarshal(buf, &temp)
if err != nil {
return err
}
// convert interface{} keys into string keys to avoid JSON errors
temp, err = convertKeys(temp)
if err != nil {
return err
}
// convert the generic map into JSON
buf, err = json.Marshal(temp)
if err != nil {
return err
}
// load JSON into destination
d := json.NewDecoder(bytes.NewReader(buf))
d.DisallowUnknownFields()
return d.Decode(dest)
}

8
internal/core/api.go

@ -245,7 +245,7 @@ func (a *api) onConfigSet(ctx *gin.Context) {
fillStruct(newConf, in) fillStruct(newConf, in)
err = newConf.CheckAndFillMissing() err = newConf.Check()
if err != nil { if err != nil {
ctx.AbortWithStatus(http.StatusBadRequest) ctx.AbortWithStatus(http.StatusBadRequest)
return return
@ -289,7 +289,7 @@ func (a *api) onConfigPathsAdd(ctx *gin.Context) {
newConf.Paths[name] = newConfPath newConf.Paths[name] = newConfPath
err = newConf.CheckAndFillMissing() err = newConf.Check()
if err != nil { if err != nil {
ctx.AbortWithStatus(http.StatusBadRequest) ctx.AbortWithStatus(http.StatusBadRequest)
return return
@ -331,7 +331,7 @@ func (a *api) onConfigPathsEdit(ctx *gin.Context) {
fillStruct(newConfPath, in) fillStruct(newConfPath, in)
err = newConf.CheckAndFillMissing() err = newConf.Check()
if err != nil { if err != nil {
ctx.AbortWithStatus(http.StatusBadRequest) ctx.AbortWithStatus(http.StatusBadRequest)
return return
@ -366,7 +366,7 @@ func (a *api) onConfigPathsDelete(ctx *gin.Context) {
delete(newConf.Paths, name) delete(newConf.Paths, name)
err := newConf.CheckAndFillMissing() err := newConf.Check()
if err != nil { if err != nil {
ctx.AbortWithStatus(http.StatusBadRequest) ctx.AbortWithStatus(http.StatusBadRequest)
return return

Loading…
Cancel
Save