Files
livekit/pkg/config/config.go
2022-10-26 14:20:13 -07:00

568 lines
16 KiB
Go

package config
import (
"fmt"
"os"
"reflect"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/pion/webrtc/v3"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
"github.com/livekit/protocol/logger"
)
var DefaultStunServers = []string{
"stun.l.google.com:19302",
"stun1.l.google.com:19302",
}
type CongestionControlProbeMode string
const (
generatedCLIFlagsHidden = true
generatedCLIFlagUsage = "generated"
CongestionControlProbeModePadding CongestionControlProbeMode = "padding"
CongestionControlProbeModeMedia CongestionControlProbeMode = "media"
StatsUpdateInterval = time.Second * 10
)
type Config struct {
Port uint32 `yaml:"port"`
BindAddresses []string `yaml:"bind_addresses"`
PrometheusPort uint32 `yaml:"prometheus_port,omitempty"`
RTC RTCConfig `yaml:"rtc,omitempty"`
Redis RedisConfig `yaml:"redis,omitempty"`
Audio AudioConfig `yaml:"audio,omitempty"`
Video VideoConfig `yaml:"video,omitempty"`
Room RoomConfig `yaml:"room,omitempty"`
TURN TURNConfig `yaml:"turn,omitempty"`
Ingress IngressConfig `yaml:"ingress,omitempty"`
WebHook WebHookConfig `yaml:"webhook,omitempty"`
NodeSelector NodeSelectorConfig `yaml:"node_selector,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
Keys map[string]string `yaml:"keys,omitempty"`
Region string `yaml:"region,omitempty"`
// LogLevel is deprecated
LogLevel string `yaml:"log_level,omitempty"`
Logging LoggingConfig `yaml:"logging,omitempty"`
Limit LimitConfig `yaml:"limit,omitempty"`
Development bool `yaml:"development,omitempty"`
}
type RTCConfig struct {
UDPPort uint32 `yaml:"udp_port,omitempty"`
TCPPort uint32 `yaml:"tcp_port,omitempty"`
ICEPortRangeStart uint32 `yaml:"port_range_start,omitempty"`
ICEPortRangeEnd uint32 `yaml:"port_range_end,omitempty"`
NodeIP string `yaml:"node_ip,omitempty"`
NodeIPAutoGenerated bool `yaml:"-"`
STUNServers []string `yaml:"stun_servers,omitempty"`
TURNServers []TURNServer `yaml:"turn_servers,omitempty"`
UseExternalIP bool `yaml:"use_external_ip"`
UseICELite bool `yaml:"use_ice_lite,omitempty"`
Interfaces InterfacesConfig `yaml:"interfaces"`
IPs IPsConfig `yaml:"ips"`
// Number of packets to buffer for NACK
PacketBufferSize int `yaml:"packet_buffer_size,omitempty"`
// Throttle periods for pli/fir rtcp packets
PLIThrottle PLIThrottleConfig `yaml:"pli_throttle,omitempty"`
CongestionControl CongestionControlConfig `yaml:"congestion_control,omitempty"`
// allow TCP and TURN/TLS fallback
AllowTCPFallback *bool `yaml:"allow_tcp_fallback,omitempty"`
// for testing, disable UDP
ForceTCP bool `yaml:"force_tcp,omitempty"`
}
type TURNServer struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Protocol string `yaml:"protocol"`
Username string `yaml:"username,omitempty"`
Credential string `yaml:"credential,omitempty"`
}
type PLIThrottleConfig struct {
LowQuality time.Duration `yaml:"low_quality,omitempty"`
MidQuality time.Duration `yaml:"mid_quality,omitempty"`
HighQuality time.Duration `yaml:"high_quality,omitempty"`
}
type CongestionControlConfig struct {
Enabled bool `yaml:"enabled"`
AllowPause bool `yaml:"allow_pause"`
UseSendSideBWE bool `yaml:"send_side_bandwidth_estimation,omitempty"`
ProbeMode CongestionControlProbeMode `yaml:"padding_mode,omitempty"`
MinChannelCapacity int64 `yaml:"min_channel_capacity,omitempty"`
}
type InterfacesConfig struct {
Includes []string `yaml:"includes"`
Excludes []string `yaml:"excludes"`
}
type IPsConfig struct {
Includes []string `yaml:"includes"`
Excludes []string `yaml:"excludes"`
}
type AudioConfig struct {
// minimum level to be considered active, 0-127, where 0 is loudest
ActiveLevel uint8 `yaml:"active_level"`
// percentile to measure, a participant is considered active if it has exceeded the ActiveLevel more than
// MinPercentile% of the time
MinPercentile uint8 `yaml:"min_percentile"`
// interval to update clients, in ms
UpdateInterval uint32 `yaml:"update_interval"`
// smoothing for audioLevel values sent to the client.
// audioLevel will be an average of `smooth_intervals`, 0 to disable
SmoothIntervals uint32 `yaml:"smooth_intervals"`
}
type VideoConfig struct {
DynacastPauseDelay time.Duration `yaml:"dynacast_pause_delay,omitempty"`
}
type RedisConfig struct {
Address string `yaml:"address"`
Username string `yaml:"username"`
Password string `yaml:"password"`
DB int `yaml:"db"`
UseTLS bool `yaml:"use_tls"`
MasterName string `yaml:"sentinel_master_name"`
SentinelUsername string `yaml:"sentinel_username"`
SentinelPassword string `yaml:"sentinel_password"`
SentinelAddresses []string `yaml:"sentinel_addresses"`
}
type RoomConfig struct {
// enable rooms to be automatically created
AutoCreate bool `yaml:"auto_create"`
EnabledCodecs []CodecSpec `yaml:"enabled_codecs"`
MaxParticipants uint32 `yaml:"max_participants"`
EmptyTimeout uint32 `yaml:"empty_timeout"`
EnableRemoteUnmute bool `yaml:"enable_remote_unmute"`
MaxMetadataSize uint32 `yaml:"max_metadata_size"`
}
type CodecSpec struct {
Mime string `yaml:"mime"`
FmtpLine string `yaml:"fmtp_line"`
}
type LoggingConfig struct {
logger.Config `yaml:",inline"`
PionLevel string `yaml:"pion_level,omitempty"`
}
type TURNConfig struct {
Enabled bool `yaml:"enabled"`
Domain string `yaml:"domain"`
CertFile string `yaml:"cert_file"`
KeyFile string `yaml:"key_file"`
TLSPort int `yaml:"tls_port"`
UDPPort int `yaml:"udp_port"`
RelayPortRangeStart uint16 `yaml:"relay_range_start,omitempty"`
RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"`
ExternalTLS bool `yaml:"external_tls"`
}
type WebHookConfig struct {
URLs []string `yaml:"urls"`
// key to use for webhook
APIKey string `yaml:"api_key"`
}
type NodeSelectorConfig struct {
Kind string `yaml:"kind"`
SortBy string `yaml:"sort_by"`
CPULoadLimit float32 `yaml:"cpu_load_limit"`
SysloadLimit float32 `yaml:"sysload_limit"`
Regions []RegionConfig `yaml:"regions"`
}
// RegionConfig lists available regions and their latitude/longitude, so the selector would prefer
// regions that are closer
type RegionConfig struct {
Name string `yaml:"name"`
Lat float64 `yaml:"lat"`
Lon float64 `yaml:"lon"`
}
type LimitConfig struct {
NumTracks int32 `yaml:"num_tracks"`
BytesPerSec float32 `yaml:"bytes_per_sec"`
}
type IngressConfig struct {
RTMPBaseURL string `yaml:"rtmp_base_url"`
}
func NewConfig(confString string, strictMode bool, c *cli.Context, baseFlags []cli.Flag) (*Config, error) {
// start with defaults
conf := &Config{
Port: 7880,
RTC: RTCConfig{
UseExternalIP: false,
TCPPort: 7881,
UDPPort: 0,
ICEPortRangeStart: 0,
ICEPortRangeEnd: 0,
STUNServers: []string{},
PacketBufferSize: 500,
PLIThrottle: PLIThrottleConfig{
LowQuality: 500 * time.Millisecond,
MidQuality: time.Second,
HighQuality: time.Second,
},
CongestionControl: CongestionControlConfig{
Enabled: true,
AllowPause: false,
ProbeMode: CongestionControlProbeModePadding,
},
},
Audio: AudioConfig{
ActiveLevel: 35, // -35dBov
MinPercentile: 40,
UpdateInterval: 400,
SmoothIntervals: 2,
},
Video: VideoConfig{
DynacastPauseDelay: 5 * time.Second,
},
Redis: RedisConfig{},
Room: RoomConfig{
AutoCreate: true,
EnabledCodecs: []CodecSpec{
{Mime: webrtc.MimeTypeOpus},
{Mime: "audio/red"},
{Mime: webrtc.MimeTypeVP8},
{Mime: webrtc.MimeTypeH264},
// {Mime: webrtc.MimeTypeAV1},
// {Mime: webrtc.MimeTypeVP9},
},
EmptyTimeout: 5 * 60,
},
Logging: LoggingConfig{
PionLevel: "error",
},
TURN: TURNConfig{
Enabled: false,
},
NodeSelector: NodeSelectorConfig{
Kind: "any",
SortBy: "random",
SysloadLimit: 0.9,
CPULoadLimit: 0.9,
},
Keys: map[string]string{},
}
if confString != "" {
decoder := yaml.NewDecoder(strings.NewReader(confString))
decoder.KnownFields(strictMode)
if err := decoder.Decode(conf); err != nil {
return nil, fmt.Errorf("could not parse config: %v", err)
}
}
if c != nil {
if err := conf.updateFromCLI(c, baseFlags); err != nil {
return nil, err
}
}
// expand env vars in filenames
file, err := homedir.Expand(os.ExpandEnv(conf.KeyFile))
if err != nil {
return nil, err
}
conf.KeyFile = file
// set defaults for ports if none are set
if conf.RTC.UDPPort == 0 && conf.RTC.ICEPortRangeStart == 0 {
// to make it easier to run in dev mode/docker, default to single port
if conf.Development {
conf.RTC.UDPPort = 7882
} else {
conf.RTC.ICEPortRangeStart = 50000
conf.RTC.ICEPortRangeEnd = 60000
}
}
// set defaults for Turn relay if none are set
if conf.TURN.RelayPortRangeStart == 0 || conf.TURN.RelayPortRangeEnd == 0 {
// to make it easier to run in dev mode/docker, default to two ports
if conf.Development {
conf.TURN.RelayPortRangeStart = 30000
conf.TURN.RelayPortRangeEnd = 30002
} else {
conf.TURN.RelayPortRangeStart = 30000
conf.TURN.RelayPortRangeEnd = 40000
}
}
if conf.RTC.NodeIP == "" {
conf.RTC.NodeIP, err = conf.determineIP()
if err != nil {
return nil, err
}
conf.RTC.NodeIPAutoGenerated = true
}
if conf.LogLevel != "" {
conf.Logging.Level = conf.LogLevel
}
if conf.Logging.Level == "" && conf.Development {
conf.Logging.Level = "debug"
}
return conf, nil
}
func (conf *Config) HasRedis() bool {
return conf.Redis.Address != "" || conf.Redis.SentinelAddresses != nil
}
func (conf *Config) UseSentinel() bool {
return conf.Redis.SentinelAddresses != nil
}
func (conf *Config) IsTURNSEnabled() bool {
if conf.TURN.Enabled && conf.TURN.TLSPort != 0 {
return true
}
for _, s := range conf.RTC.TURNServers {
if s.Protocol == "tls" {
return true
}
}
return false
}
type configNode struct {
TypeNode reflect.Value
TagPrefix string
}
func (conf *Config) ToCLIFlagNames(existingFlags []cli.Flag) map[string]reflect.Value {
existingFlagNames := map[string]bool{}
for _, flag := range existingFlags {
for _, flagName := range flag.Names() {
existingFlagNames[flagName] = true
}
}
flagNames := map[string]reflect.Value{}
var currNode configNode
nodes := []configNode{{reflect.ValueOf(conf).Elem(), ""}}
for len(nodes) > 0 {
currNode, nodes = nodes[0], nodes[1:]
for i := 0; i < currNode.TypeNode.NumField(); i++ {
// inspect yaml tag from struct field to get path
field := currNode.TypeNode.Type().Field(i)
yamlTag := strings.SplitN(field.Tag.Get("yaml"), ",", 2)[0]
if yamlTag == "" || yamlTag == "-" {
continue
}
yamlPath := yamlTag
if currNode.TagPrefix != "" {
yamlPath = fmt.Sprintf("%s.%s", currNode.TagPrefix, yamlTag)
}
if existingFlagNames[yamlPath] {
continue
}
// map flag name to value
value := currNode.TypeNode.Field(i)
if value.Kind() == reflect.Struct {
nodes = append(nodes, configNode{value, yamlPath})
} else {
flagNames[yamlPath] = value
}
}
}
return flagNames
}
func GenerateCLIFlags(existingFlags []cli.Flag) ([]cli.Flag, error) {
blankConfig := &Config{}
flags := []cli.Flag{}
for name, value := range blankConfig.ToCLIFlagNames(existingFlags) {
kind := value.Kind()
if kind == reflect.Ptr {
kind = value.Type().Elem().Kind()
}
var flag cli.Flag
envVar := fmt.Sprintf("LIVEKIT_%s", strings.ToUpper(strings.Replace(name, ".", "_", -1)))
switch kind {
case reflect.Bool:
flag = &cli.BoolFlag{
Name: name,
Usage: generatedCLIFlagUsage,
Hidden: generatedCLIFlagsHidden,
}
case reflect.String:
flag = &cli.StringFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: generatedCLIFlagsHidden,
}
case reflect.Int, reflect.Int32:
flag = &cli.IntFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: generatedCLIFlagsHidden,
}
case reflect.Int64:
flag = &cli.Int64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: generatedCLIFlagsHidden,
}
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
flag = &cli.UintFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: generatedCLIFlagsHidden,
}
case reflect.Float32:
flag = &cli.Float64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: generatedCLIFlagsHidden,
}
case reflect.Slice:
// TODO
continue
case reflect.Map:
// TODO
continue
default:
return flags, fmt.Errorf("cli flag generation unsupported for config type: %s is a %s", name, kind.String())
}
flags = append(flags, flag)
}
return flags, nil
}
func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
generatedFlagNames := conf.ToCLIFlagNames(baseFlags)
for _, flag := range c.App.Flags {
flagName := flag.Names()[0]
// the `c.App.Name != "test"` check is needed because `c.IsSet(...)` is always false in unit tests
if !c.IsSet(flagName) && c.App.Name != "test" {
continue
}
configValue, ok := generatedFlagNames[flagName]
if !ok {
continue
}
kind := configValue.Kind()
if kind == reflect.Ptr {
// instantiate value to be set
configValue.Set(reflect.New(configValue.Type().Elem()))
kind = configValue.Type().Elem().Kind()
configValue = configValue.Elem()
}
switch kind {
case reflect.Bool:
configValue.SetBool(c.Bool(flagName))
case reflect.String:
configValue.SetString(c.String(flagName))
case reflect.Int, reflect.Int32, reflect.Int64:
configValue.SetInt(c.Int64(flagName))
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
configValue.SetUint(c.Uint64(flagName))
case reflect.Float32:
configValue.SetFloat(c.Float64(flagName))
// case reflect.Slice:
// // TODO
// case reflect.Map:
// // TODO
default:
return fmt.Errorf("unsupported generated cli flag type for config: %s is a %s", flagName, kind.String())
}
}
if c.IsSet("dev") {
conf.Development = c.Bool("dev")
}
if c.IsSet("key-file") {
conf.KeyFile = c.String("key-file")
}
if c.IsSet("keys") {
if err := conf.unmarshalKeys(c.String("keys")); err != nil {
return errors.New("Could not parse keys, it needs to be exactly, \"key: secret\", including the space")
}
}
if c.IsSet("region") {
conf.Region = c.String("region")
}
if c.IsSet("redis-host") {
conf.Redis.Address = c.String("redis-host")
}
if c.IsSet("redis-password") {
conf.Redis.Password = c.String("redis-password")
}
if c.IsSet("turn-cert") {
conf.TURN.CertFile = c.String("turn-cert")
}
if c.IsSet("turn-key") {
conf.TURN.KeyFile = c.String("turn-key")
}
if c.IsSet("node-ip") {
conf.RTC.NodeIP = c.String("node-ip")
}
if c.IsSet("udp-port") {
conf.RTC.UDPPort = uint32(c.Int("udp-port"))
}
if c.IsSet("bind") {
conf.BindAddresses = c.StringSlice("bind")
}
return nil
}
func (conf *Config) unmarshalKeys(keys string) error {
temp := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(keys), temp); err != nil {
return err
}
conf.Keys = make(map[string]string, len(temp))
for key, val := range temp {
if secret, ok := val.(string); ok {
conf.Keys[key] = secret
}
}
return nil
}