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 @@ -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)
* [HLS standards](https://github.com/bluenviron/gohlslib#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)
* [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 { @@ -41,6 +41,8 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error {
return err
}
*d = nil
for _, v := range in {
switch v {
case "basic":
@ -57,8 +59,8 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error { @@ -57,8 +59,8 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error {
return nil
}
// unmarshalEnv implements envUnmarshaler.
func (d *AuthMethods) unmarshalEnv(s string) error {
// UnmarshalEnv implements envUnmarshaler.
func (d *AuthMethods) UnmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts)
}

341
internal/conf/conf.go

@ -2,11 +2,10 @@ @@ -2,11 +2,10 @@
package conf
import (
"encoding/base64"
"bytes"
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strings"
"time"
@ -14,29 +13,22 @@ import ( @@ -14,29 +13,22 @@ import (
"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/conf/decrypt"
"github.com/aler9/mediamtx/internal/conf/env"
"github.com/aler9/mediamtx/internal/conf/yaml"
"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")
func getSortedKeys(paths map[string]*PathConf) []string {
ret := make([]string, len(paths))
i := 0
for name := range paths {
ret[i] = name
i++
}
return decrypted, nil
sort.Strings(ret)
return ret
}
func loadFromFile(fpath string, conf *Conf) (bool, error) {
@ -52,6 +44,7 @@ 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
if fpath == "mediamtx.yml" || fpath == "rtsp-simple-server.yml" {
if _, err := os.Stat(fpath); err != nil {
conf.UnmarshalJSON(nil) // load defaults
return false, nil
}
}
@ -62,119 +55,20 @@ func loadFromFile(fpath string, conf *Conf) (bool, error) { @@ -62,119 +55,20 @@ func loadFromFile(fpath string, conf *Conf) (bool, error) {
}
if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { // legacy format
byts, err = decrypt(key, byts)
byts, err = decrypt.Decrypt(key, byts)
if err != nil {
return true, err
}
}
if key, ok := os.LookupEnv("MTX_CONFKEY"); ok {
byts, err = decrypt(key, byts)
byts, err = decrypt.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)
err = yaml.Load(byts, conf)
if err != nil {
return true, err
}
@ -267,17 +161,17 @@ func Load(fpath string) (*Conf, bool, error) { @@ -267,17 +161,17 @@ func Load(fpath string) (*Conf, bool, error) {
return nil, false, err
}
err = loadFromEnvironment("RTSP", conf) // legacy prefix
err = env.Load("RTSP", conf) // legacy prefix
if err != nil {
return nil, false, err
}
err = loadFromEnvironment("MTX", conf)
err = env.Load("MTX", conf)
if err != nil {
return nil, false, err
}
err = conf.CheckAndFillMissing()
err = conf.Check()
if err != nil {
return nil, false, err
}
@ -301,33 +195,12 @@ func (conf Conf) Clone() *Conf { @@ -301,33 +195,12 @@ func (conf Conf) Clone() *Conf {
return &dest
}
// CheckAndFillMissing checks the configuration for errors and fills missing parameters.
func (conf *Conf) CheckAndFillMissing() error {
// Check checks the configuration for errors.
func (conf *Conf) Check() 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")
}
@ -337,24 +210,8 @@ func (conf *Conf) CheckAndFillMissing() error { @@ -337,24 +210,8 @@ func (conf *Conf) CheckAndFillMissing() error {
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")
@ -364,90 +221,6 @@ func (conf *Conf) CheckAndFillMissing() error { @@ -364,90 +221,6 @@ func (conf *Conf) CheckAndFillMissing() error {
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.
@ -461,22 +234,15 @@ func (conf *Conf) CheckAndFillMissing() error { @@ -461,22 +234,15 @@ func (conf *Conf) CheckAndFillMissing() error {
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 {
for _, name := range getSortedKeys(conf.Paths) {
pconf := conf.Paths[name]
if pconf == nil {
pconf = &PathConf{}
pconf.UnmarshalJSON(nil) // fill defaults
conf.Paths[name] = pconf
}
err := pconf.checkAndFillMissing(conf, name)
err := pconf.check(conf, name)
if err != nil {
return err
}
@ -484,3 +250,62 @@ func (conf *Conf) CheckAndFillMissing() error { @@ -484,3 +250,62 @@ func (conf *Conf) CheckAndFillMissing() error {
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) { @@ -51,6 +51,17 @@ func TestConfFromFile(t *testing.T) {
Source: "publisher",
SourceOnDemandStartTimeout: 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),
RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
}, pa)
@ -109,6 +120,17 @@ func TestConfFromFileAndEnv(t *testing.T) { @@ -109,6 +120,17 @@ func TestConfFromFileAndEnv(t *testing.T) {
Source: "rtsp://testing",
SourceOnDemandStartTimeout: 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),
RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
}, pa)
@ -128,6 +150,17 @@ func TestConfFromEnvOnly(t *testing.T) { @@ -128,6 +150,17 @@ func TestConfFromEnvOnly(t *testing.T) {
Source: "rtsp://testing",
SourceOnDemandStartTimeout: 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),
RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
}, pa)
@ -179,14 +212,14 @@ func TestConfErrors(t *testing.T) { @@ -179,14 +212,14 @@ func TestConfErrors(t *testing.T) {
{
"non existent parameter 1",
`invalid: param`,
"non-existent parameter: 'invalid'",
"json: unknown field \"invalid\"",
},
{
"non existent parameter 2",
"paths:\n" +
" mypath:\n" +
" invalid: parameter\n",
"parameter paths, key mypath: non-existent parameter: 'invalid'",
"json: unknown field \"invalid\"",
},
{
"invalid path name",

4
internal/conf/credential.go

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

29
internal/conf/decrypt/decrypt.go

@ -0,0 +1,29 @@ @@ -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 { @@ -60,7 +60,7 @@ func (d *Encryption) UnmarshalJSON(b []byte) error {
return nil
}
// unmarshalEnv implements envUnmarshaler.
func (d *Encryption) unmarshalEnv(s string) error {
// UnmarshalEnv implements envUnmarshaler.
func (d *Encryption) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`))
}

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

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
package conf
// Package env contains a function to load configuration from environment.
package env
import (
"encoding/json"
"fmt"
"os"
"reflect"
@ -9,7 +11,7 @@ import ( @@ -9,7 +11,7 @@ import (
)
type envUnmarshaler interface {
unmarshalEnv(string) error
UnmarshalEnv(string) 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 @@ -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 ev, ok := env[prefix]; ok {
err := i.unmarshalEnv(ev)
err := i.UnmarshalEnv(ev)
if err != nil {
return fmt.Errorf("%s: %s", prefix, err)
}
@ -105,6 +107,9 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err @@ -105,6 +107,9 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
zero := reflect.Value{}
if nv == zero {
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)
}
@ -148,7 +153,8 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err @@ -148,7 +153,8 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
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)
for _, kv := range os.Environ() {
tmp := strings.SplitN(kv, "=", 2)

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

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
package conf
package env
import (
"encoding/json"
"os"
"testing"
"time"
@ -17,18 +18,40 @@ type mapEntry struct { @@ -17,18 +18,40 @@ type mapEntry struct {
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 {
MyString string
MyInt int
MyFloat float64
MyBool bool
MyDuration StringDuration
MyDuration myDuration
MyMap map[string]*mapEntry
MySlice []string
MySliceEmpty []string
}
func TestEnvironment(t *testing.T) {
func TestLoad(t *testing.T) {
os.Setenv("MYPREFIX_MYSTRING", "testcontent")
defer os.Unsetenv("MYPREFIX_MYSTRING")
@ -60,14 +83,14 @@ func TestEnvironment(t *testing.T) { @@ -60,14 +83,14 @@ func TestEnvironment(t *testing.T) {
defer os.Unsetenv("MYPREFIX_MYSLICEEMPTY")
var s testStruct
err := loadFromEnvironment("MYPREFIX", &s)
err := Load("MYPREFIX", &s)
require.NoError(t, err)
require.Equal(t, "testcontent", s.MyString)
require.Equal(t, 123, s.MyInt)
require.Equal(t, 15.2, s.MyFloat)
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"]
require.Equal(t, true, ok)

4
internal/conf/hlsvariant.go

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

6
internal/conf/ipsorcidrs.go

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

6
internal/conf/logdestination.go

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

4
internal/conf/loglevel.go

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

85
internal/conf/path.go

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
package conf
import (
"bytes"
"encoding/json"
"fmt"
"net"
@ -104,7 +105,7 @@ type PathConf struct { @@ -104,7 +105,7 @@ type PathConf struct {
RunOnReadRestart bool `json:"runOnReadRestart"`
}
func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
func (pconf *PathConf) check(conf *Conf, name string) error {
// normal path
if name == "" || name[0] != '~' {
err := IsValidPathName(name)
@ -121,10 +122,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error { @@ -121,10 +122,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
pconf.Regexp = pathRegexp
}
if pconf.Source == "" {
pconf.Source = "publisher"
}
switch {
case pconf.Source == "publisher":
@ -221,40 +218,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error { @@ -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:
return fmt.Errorf("invalid source: '%s'", pconf.Source)
}
@ -265,14 +228,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error { @@ -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 strings.HasPrefix(pconf.Fallback, "/") {
err := IsValidPathName(pconf.Fallback[1:])
@ -331,14 +286,6 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error { @@ -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'")
}
if pconf.RunOnDemandStartTimeout == 0 {
pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second)
}
if pconf.RunOnDemandCloseAfter == 0 {
pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second)
}
return nil
}
@ -384,3 +331,31 @@ func (pconf PathConf) HasOnDemandStaticSource() bool { @@ -384,3 +331,31 @@ func (pconf PathConf) HasOnDemandStaticSource() bool {
func (pconf PathConf) HasOnDemandPublisher() bool {
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 { @@ -74,8 +74,8 @@ func (d *Protocols) UnmarshalJSON(b []byte) error {
return nil
}
// unmarshalEnv implements envUnmarshaler.
func (d *Protocols) unmarshalEnv(s string) error {
// UnmarshalEnv implements envUnmarshaler.
func (d *Protocols) UnmarshalEnv(s string) error {
byts, _ := json.Marshal(strings.Split(s, ","))
return d.UnmarshalJSON(byts)
}

4
internal/conf/sourceprotocol.go

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

4
internal/conf/stringduration.go

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

4
internal/conf/stringsize.go

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

70
internal/conf/yaml/load.go

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

Loading…
Cancel
Save