// Copyright 2023 LiveKit, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "fmt" "os" "reflect" "strconv" "strings" "time" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/urfave/cli/v3" "gopkg.in/yaml.v3" "github.com/livekit/mediatransportutil/pkg/rtcconfig" "github.com/livekit/protocol/codecs/mime" "github.com/livekit/protocol/livekit" "github.com/livekit/protocol/logger" redisLiveKit "github.com/livekit/protocol/redis" "github.com/livekit/protocol/rpc" "github.com/livekit/protocol/webhook" "github.com/livekit/livekit-server/pkg/agent" "github.com/livekit/livekit-server/pkg/metric" "github.com/livekit/livekit-server/pkg/sfu" "github.com/livekit/livekit-server/pkg/sfu/bwe/remotebwe" "github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe" "github.com/livekit/livekit-server/pkg/sfu/pacer" "github.com/livekit/livekit-server/pkg/sfu/streamallocator" ) const ( generatedCLIFlagUsage = "generated" ) var ( ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0") ErrKeysNotSet = errors.New("one of key-file or keys must be provided") ) type Config struct { Port uint32 `yaml:"port,omitempty"` BindAddresses []string `yaml:"bind_addresses,omitempty"` // PrometheusPort is deprecated PrometheusPort uint32 `yaml:"prometheus_port,omitempty"` Prometheus PrometheusConfig `yaml:"prometheus,omitempty"` RTC RTCConfig `yaml:"rtc,omitempty"` Redis redisLiveKit.RedisConfig `yaml:"redis,omitempty"` Audio sfu.AudioConfig `yaml:"audio,omitempty"` Video VideoConfig `yaml:"video,omitempty"` Room RoomConfig `yaml:"room,omitempty"` TURN TURNConfig `yaml:"turn,omitempty"` Ingress IngressConfig `yaml:"ingress,omitempty"` SIP SIPConfig `yaml:"sip,omitempty"` WebHook 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"` SignalRelay SignalRelayConfig `yaml:"signal_relay,omitempty"` PSRPC rpc.PSRPCConfig `yaml:"psrpc,omitempty"` // Deprecated: LogLevel is deprecated LogLevel string `yaml:"log_level,omitempty"` Logging LoggingConfig `yaml:"logging,omitempty"` Limit LimitConfig `yaml:"limit,omitempty"` Agents agent.Config `yaml:"agents,omitempty"` Development bool `yaml:"development,omitempty"` Metric metric.MetricConfig `yaml:"metric,omitempty"` Trace TracingConfig `yaml:"trace,omitempty"` NodeStats NodeStatsConfig `yaml:"node_stats,omitempty"` EnableDataTracks bool `yaml:"enable_data_tracks,omitempty"` API APIConfig `yaml:"api,omitempty"` } type RTCConfig struct { rtcconfig.RTCConfig `yaml:",inline"` TURNServers []TURNServer `yaml:"turn_servers,omitempty"` // Deprecated StrictACKs bool `yaml:"strict_acks,omitempty"` // Deprecated: use PacketBufferSizeVideo and PacketBufferSizeAudio PacketBufferSize int `yaml:"packet_buffer_size,omitempty"` // Number of packets to buffer for NACK - video PacketBufferSizeVideo int `yaml:"packet_buffer_size_video,omitempty"` // Number of packets to buffer for NACK - audio PacketBufferSizeAudio int `yaml:"packet_buffer_size_audio,omitempty"` // Throttle periods for pli/fir rtcp packets PLIThrottle sfu.PLIThrottleConfig `yaml:"pli_throttle,omitempty"` CongestionControl CongestionControlConfig `yaml:"congestion_control,omitempty"` // allow TCP and TURN/TLS fallback AllowTCPFallback *bool `yaml:"allow_tcp_fallback,omitempty"` // force a reconnect on a publication error ReconnectOnPublicationError *bool `yaml:"reconnect_on_publication_error,omitempty"` // force a reconnect on a subscription error ReconnectOnSubscriptionError *bool `yaml:"reconnect_on_subscription_error,omitempty"` // force a reconnect on a data channel error ReconnectOnDataChannelError *bool `yaml:"reconnect_on_data_channel_error,omitempty"` // Deprecated DataChannelMaxBufferedAmount uint64 `yaml:"data_channel_max_buffered_amount,omitempty"` // Threshold of data channel writing to be considered too slow, data packet could // be dropped for a slow data channel to avoid blocking the room. DatachannelSlowThreshold int `yaml:"datachannel_slow_threshold,omitempty"` // Target latency for lossy data channels, used to drop packets to reduce latency. DatachannelLossyTargetLatency time.Duration `yaml:"datachannel_lossy_target_latency,omitempty"` ForwardStats ForwardStatsConfig `yaml:"forward_stats,omitempty"` // enable rtp stream restart detection for published tracks EnableRTPStreamRestartDetection bool `yaml:"enable_rtp_stream_restart_detection,omitempty"` } type TURNServer struct { Host string `yaml:"host,omitempty"` Port int `yaml:"port,omitempty"` Protocol string `yaml:"protocol,omitempty"` Username string `yaml:"username,omitempty"` Credential string `yaml:"credential,omitempty"` // Secret is used for TURN static auth secrets mechanism. When provided, // dynamic credentials are generated using HMAC-SHA1 instead of static Username/Credential Secret string `yaml:"secret,omitempty"` // TTL is the time-to-live in seconds for generated credentials when using Secret. // Defaults to 14400 seconds (4 hours) if not specified TTL int `yaml:"ttl,omitempty"` } type CongestionControlConfig struct { Enabled bool `yaml:"enabled,omitempty"` AllowPause bool `yaml:"allow_pause,omitempty"` StreamAllocator streamallocator.StreamAllocatorConfig `yaml:"stream_allocator,omitempty"` RemoteBWE remotebwe.RemoteBWEConfig `yaml:"remote_bwe,omitempty"` UseSendSideBWEInterceptor bool `yaml:"use_send_side_bwe_interceptor,omitempty"` UseSendSideBWE bool `yaml:"use_send_side_bwe,omitempty"` SendSideBWEPacer string `yaml:"send_side_bwe_pacer,omitempty"` SendSideBWE sendsidebwe.SendSideBWEConfig `yaml:"send_side_bwe,omitempty"` } type PlayoutDelayConfig struct { Enabled bool `yaml:"enabled,omitempty"` Min int `yaml:"min,omitempty"` Max int `yaml:"max,omitempty"` } type VideoConfig struct { DynacastPauseDelay time.Duration `yaml:"dynacast_pause_delay,omitempty"` StreamTrackerManager sfu.StreamTrackerManagerConfig `yaml:"stream_tracker_manager,omitempty"` CodecRegressionThreshold int `yaml:"codec_regression_threshold,omitempty"` } type RoomConfig struct { // enable rooms to be automatically created AutoCreate bool `yaml:"auto_create,omitempty"` EnabledCodecs []CodecSpec `yaml:"enabled_codecs,omitempty"` MaxParticipants uint32 `yaml:"max_participants,omitempty"` EmptyTimeout uint32 `yaml:"empty_timeout,omitempty"` DepartureTimeout uint32 `yaml:"departure_timeout,omitempty"` EnableRemoteUnmute bool `yaml:"enable_remote_unmute,omitempty"` PlayoutDelay PlayoutDelayConfig `yaml:"playout_delay,omitempty"` SyncStreams bool `yaml:"sync_streams,omitempty"` CreateRoomEnabled bool `yaml:"create_room_enabled,omitempty"` CreateRoomTimeout time.Duration `yaml:"create_room_timeout,omitempty"` CreateRoomAttempts int `yaml:"create_room_attempts,omitempty"` // target room participant update batch chunk size in bytes UpdateBatchTargetSize int `yaml:"update_batch_target_size,omitempty"` // deprecated, moved to limits MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"` // deprecated, moved to limits MaxRoomNameLength int `yaml:"max_room_name_length,omitempty"` // deprecated, moved to limits MaxParticipantIdentityLength int `yaml:"max_participant_identity_length,omitempty"` RoomConfigurations map[string]*livekit.RoomConfiguration `yaml:"room_configurations,omitempty"` } type CodecSpec struct { Mime string `yaml:"mime,omitempty"` FmtpLine string `yaml:"fmtp_line,omitempty"` } type LoggingConfig struct { logger.Config `yaml:",inline"` PionLevel string `yaml:"pion_level,omitempty"` } type TURNConfig struct { Enabled bool `yaml:"enabled,omitempty"` Domain string `yaml:"domain,omitempty"` CertFile string `yaml:"cert_file,omitempty"` KeyFile string `yaml:"key_file,omitempty"` TLSPort int `yaml:"tls_port,omitempty"` UDPPort int `yaml:"udp_port,omitempty"` RelayPortRangeStart uint16 `yaml:"relay_range_start,omitempty"` RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"` ExternalTLS bool `yaml:"external_tls,omitempty"` BindAddresses []string `yaml:"bind_addresses,omitempty"` // TTL of the TURN credentials in seconds - defaults to 300 TTLSeconds int `yaml:"ttl_seconds,omitempty"` // list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to. // By default (i. e. empty list), all restricted peer CIDRs are denied access. // When not empty, only the specified CIDRs are allowed access. // Note that this check is applied to restricted peer CIDRs only. AllowRestrictedPeerCIDRs []string `yaml:"allow_restricted_peer_cidrs,omitempty"` // list of peer CIDRs to deny access to // This applies to all peer CIDRs, including restricted ones. // Deny list takes precedence over allow list. DenyPeerCIDRs []string `yaml:"deny_peer_cidrs,omitempty"` } type NodeSelectorConfig struct { Kind string `yaml:"kind,omitempty"` SortBy string `yaml:"sort_by,omitempty"` Algorithm string `yaml:"algorithm,omitempty"` CPULoadLimit float32 `yaml:"cpu_load_limit,omitempty"` SysloadLimit float32 `yaml:"sysload_limit,omitempty"` Regions []RegionConfig `yaml:"regions,omitempty"` } type SignalRelayConfig struct { RetryTimeout time.Duration `yaml:"retry_timeout,omitempty"` MinRetryInterval time.Duration `yaml:"min_retry_interval,omitempty"` MaxRetryInterval time.Duration `yaml:"max_retry_interval,omitempty"` StreamBufferSize int `yaml:"stream_buffer_size,omitempty"` ConnectAttempts int `yaml:"connect_attempts,omitempty"` } // RegionConfig lists available regions and their latitude/longitude, so the selector would prefer // regions that are closer type RegionConfig struct { Name string `yaml:"name,omitempty"` Lat float64 `yaml:"lat,omitempty"` Lon float64 `yaml:"lon,omitempty"` } type LimitConfig struct { NumTracks int32 `yaml:"num_tracks,omitempty"` BytesPerSec float32 `yaml:"bytes_per_sec,omitempty"` SubscriptionLimitVideo int32 `yaml:"subscription_limit_video,omitempty"` SubscriptionLimitAudio int32 `yaml:"subscription_limit_audio,omitempty"` MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"` // total size of all attributes on a participant MaxAttributesSize uint32 `yaml:"max_attributes_size,omitempty"` MaxRoomNameLength int `yaml:"max_room_name_length,omitempty"` MaxParticipantIdentityLength int `yaml:"max_participant_identity_length,omitempty"` MaxParticipantNameLength int `yaml:"max_participant_name_length,omitempty"` } func (l LimitConfig) CheckRoomNameLength(name string) bool { return l.MaxRoomNameLength == 0 || len(name) <= l.MaxRoomNameLength } func (l LimitConfig) CheckParticipantIdentityLength(identity string) bool { return l.MaxParticipantIdentityLength == 0 || len(identity) <= l.MaxParticipantIdentityLength } func (l LimitConfig) CheckParticipantNameLength(name string) bool { return l.MaxParticipantNameLength == 0 || len(name) <= l.MaxParticipantNameLength } func (l LimitConfig) CheckMetadataSize(metadata string) bool { return l.MaxMetadataSize == 0 || uint32(len(metadata)) <= l.MaxMetadataSize } func (l LimitConfig) CheckAttributesSize(attributes map[string]string) bool { if l.MaxAttributesSize == 0 { return true } total := 0 for k, v := range attributes { total += len(k) + len(v) } return uint32(total) <= l.MaxAttributesSize } type IngressConfig struct { RTMPBaseURL string `yaml:"rtmp_base_url,omitempty"` WHIPBaseURL string `yaml:"whip_base_url,omitempty"` } type SIPConfig struct{} type APIConfig struct { // amount of time to wait for API to execute, default 2s ExecutionTimeout time.Duration `yaml:"execution_timeout,omitempty"` // min amount of time to wait before checking for operation complete CheckInterval time.Duration `yaml:"check_interval,omitempty"` // max amount of time to wait before checking for operation complete MaxCheckInterval time.Duration `yaml:"max_check_interval,omitempty"` // Backwards compatibility for room service api calls, will enable by default and remove in a future release EnablePsrpcForGetListParticpants bool `yaml:"enable_psrpc_for_get_list_participants,omitempty"` } type PrometheusConfig struct { Port uint32 `yaml:"port,omitempty"` Username string `yaml:"username,omitempty"` Password string `yaml:"password,omitempty"` } type ForwardStatsConfig struct { SummaryInterval time.Duration `yaml:"summary_interval,omitempty"` ReportInterval time.Duration `yaml:"report_interval,omitempty"` ReportWindow time.Duration `yaml:"report_window,omitempty"` } type TracingConfig struct { // JaegerURL configures Jaeger as a global tracer. // // The following formats are supported: , :, http(s):/// JaegerURL string `yaml:"jaeger_url,omitempty"` } func DefaultAPIConfig() APIConfig { return APIConfig{ ExecutionTimeout: 2 * time.Second, CheckInterval: 100 * time.Millisecond, MaxCheckInterval: 300 * time.Second, } } type NodeStatsConfig struct { StatsUpdateInterval time.Duration `yaml:"stats_update_interval,omitempty"` StatsRateMeasurementIntervals []time.Duration `yaml:"stats_rate_measurement_intervals,omitempty"` StatsMaxDelay time.Duration `yaml:"stats_max_delay,omitempty"` } var DefaultNodeStatsConfig = NodeStatsConfig{ StatsUpdateInterval: 2 * time.Second, StatsRateMeasurementIntervals: []time.Duration{10 * time.Second}, StatsMaxDelay: 30 * time.Second, } var DefaultConfig = Config{ Port: 7880, RTC: RTCConfig{ RTCConfig: rtcconfig.RTCConfig{ UseExternalIP: false, TCPPort: 7881, ICEPortRangeStart: 0, ICEPortRangeEnd: 0, STUNServers: []string{}, }, PacketBufferSize: 500, PacketBufferSizeVideo: 500, PacketBufferSizeAudio: 200, PLIThrottle: sfu.DefaultPLIThrottleConfig, CongestionControl: CongestionControlConfig{ Enabled: true, AllowPause: false, StreamAllocator: streamallocator.DefaultStreamAllocatorConfig, RemoteBWE: remotebwe.DefaultRemoteBWEConfig, UseSendSideBWEInterceptor: false, UseSendSideBWE: false, SendSideBWEPacer: string(pacer.PacerBehaviorNoQueue), SendSideBWE: sendsidebwe.DefaultSendSideBWEConfig, }, }, Audio: sfu.DefaultAudioConfig, Video: VideoConfig{ DynacastPauseDelay: 5 * time.Second, StreamTrackerManager: sfu.DefaultStreamTrackerManagerConfig, CodecRegressionThreshold: 5, }, Redis: redisLiveKit.RedisConfig{}, Room: RoomConfig{ AutoCreate: true, EnabledCodecs: []CodecSpec{ {Mime: mime.MimeTypePCMU.String()}, {Mime: mime.MimeTypePCMA.String()}, {Mime: mime.MimeTypeOpus.String()}, {Mime: mime.MimeTypeRED.String()}, {Mime: mime.MimeTypeVP8.String()}, {Mime: mime.MimeTypeH264.String()}, {Mime: mime.MimeTypeVP9.String()}, {Mime: mime.MimeTypeAV1.String()}, {Mime: mime.MimeTypeH265.String()}, {Mime: mime.MimeTypeRTX.String()}, }, EmptyTimeout: 5 * 60, DepartureTimeout: 20, CreateRoomEnabled: true, CreateRoomTimeout: 10 * time.Second, CreateRoomAttempts: 3, UpdateBatchTargetSize: 128 * 1024, }, Limit: LimitConfig{ MaxMetadataSize: 64000, MaxAttributesSize: 64000, MaxRoomNameLength: 256, MaxParticipantIdentityLength: 256, MaxParticipantNameLength: 256, }, Logging: LoggingConfig{ PionLevel: "error", }, TURN: TURNConfig{ Enabled: false, BindAddresses: []string{"0.0.0.0"}, TTLSeconds: 300, }, NodeSelector: NodeSelectorConfig{ Kind: "any", SortBy: "random", SysloadLimit: 0.9, CPULoadLimit: 0.9, Algorithm: "lowest", }, SignalRelay: SignalRelayConfig{ RetryTimeout: 7500 * time.Millisecond, MinRetryInterval: 500 * time.Millisecond, MaxRetryInterval: 4 * time.Second, StreamBufferSize: 1000, ConnectAttempts: 3, }, Agents: agent.Config{ TargetLoad: agent.DefaultTargetLoad, }, PSRPC: rpc.DefaultPSRPCConfig, Keys: map[string]string{}, Metric: metric.DefaultMetricConfig, WebHook: webhook.DefaultWebHookConfig, NodeStats: DefaultNodeStatsConfig, API: DefaultAPIConfig(), EnableDataTracks: true, } func NewConfig(confString string, strictMode bool, c *cli.Command, baseFlags []cli.Flag) (*Config, error) { // start with defaults marshalled, err := yaml.Marshal(&DefaultConfig) if err != nil { return nil, err } var conf Config err = yaml.Unmarshal(marshalled, &conf) if err != nil { return nil, err } 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 } } if err := conf.RTC.Validate(conf.Development); err != nil { return nil, fmt.Errorf("could not validate RTC config: %v", 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 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.LogLevel != "" { conf.Logging.Level = conf.LogLevel } if conf.Logging.Level == "" && conf.Development { conf.Logging.Level = "debug" } if conf.Logging.PionLevel != "" { if conf.Logging.ComponentLevels == nil { conf.Logging.ComponentLevels = map[string]string{} } conf.Logging.ComponentLevels["transport.pion"] = conf.Logging.PionLevel conf.Logging.ComponentLevels["pion"] = conf.Logging.PionLevel } // copy over legacy limits if conf.Room.MaxMetadataSize != 0 { conf.Limit.MaxMetadataSize = conf.Room.MaxMetadataSize } if conf.Room.MaxParticipantIdentityLength != 0 { conf.Limit.MaxParticipantIdentityLength = conf.Room.MaxParticipantIdentityLength } if conf.Room.MaxRoomNameLength != 0 { conf.Limit.MaxRoomNameLength = conf.Room.MaxRoomNameLength } return &conf, 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) yamlTagArray := strings.SplitN(field.Tag.Get("yaml"), ",", 2) yamlTag := yamlTagArray[0] isInline := false if len(yamlTagArray) > 1 && yamlTagArray[1] == "inline" { isInline = true } if (yamlTag == "" && (!isInline || currNode.TagPrefix == "")) || yamlTag == "-" { continue } yamlPath := yamlTag if currNode.TagPrefix != "" { if isInline { yamlPath = currNode.TagPrefix } else { 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 (conf *Config) ValidateKeys() error { // prefer keyfile if set if conf.KeyFile != "" { var otherFilter os.FileMode = 0o007 if st, err := os.Stat(conf.KeyFile); err != nil { return err } else if st.Mode().Perm()&otherFilter != 0o000 { return ErrKeyFileIncorrectPermission } f, err := os.Open(conf.KeyFile) if err != nil { return err } defer func() { _ = f.Close() }() decoder := yaml.NewDecoder(f) conf.Keys = map[string]string{} if err = decoder.Decode(conf.Keys); err != nil { return err } } if len(conf.Keys) == 0 { return ErrKeysNotSet } if !conf.Development { for key, secret := range conf.Keys { if len(secret) < 32 { logger.Errorw("secret is too short, should be at least 32 characters for security", nil, "apiKey", key) } } } return nil } func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error) { defaultConfig := &DefaultConfig flags := make([]cli.Flag, 0) for name, value := range (defaultConfig).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.ReplaceAll(name, ".", "_"))) defaultText := cliDefaultText(value) switch kind { case reflect.Bool: flag = &cli.BoolFlag{ Name: name, Sources: cli.EnvVars(envVar), Usage: generatedCLIFlagUsage, DefaultText: defaultText, Hidden: hidden, } case reflect.String: flag = &cli.StringFlag{ Name: name, Sources: cli.EnvVars(envVar), Usage: generatedCLIFlagUsage, DefaultText: defaultText, Hidden: hidden, } case reflect.Int, reflect.Int32: flag = &cli.IntFlag{ Name: name, Sources: cli.EnvVars(envVar), Usage: generatedCLIFlagUsage, DefaultText: defaultText, Hidden: hidden, } case reflect.Int64: flag = &cli.Int64Flag{ Name: name, Sources: cli.EnvVars(envVar), Usage: generatedCLIFlagUsage, DefaultText: defaultText, Hidden: hidden, } case reflect.Uint8, reflect.Uint16, reflect.Uint32: flag = &cli.UintFlag{ Name: name, Sources: cli.EnvVars(envVar), Usage: generatedCLIFlagUsage, DefaultText: defaultText, Hidden: hidden, } case reflect.Uint64: flag = &cli.Uint64Flag{ Name: name, Sources: cli.EnvVars(envVar), Usage: generatedCLIFlagUsage, DefaultText: defaultText, Hidden: hidden, } case reflect.Float32: flag = &cli.Float64Flag{ Name: name, Sources: cli.EnvVars(envVar), Usage: generatedCLIFlagUsage, DefaultText: defaultText, Hidden: hidden, } case reflect.Float64: flag = &cli.Float64Flag{ Name: name, Sources: cli.EnvVars(envVar), Usage: generatedCLIFlagUsage, DefaultText: defaultText, Hidden: hidden, } case reflect.Slice: // TODO continue case reflect.Map: // TODO continue case reflect.Struct: // 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 cliDefaultText(value reflect.Value) string { if value.Kind() == reflect.Ptr { if value.IsNil() { return "" } value = value.Elem() } switch value.Kind() { case reflect.Bool: return strconv.FormatBool(value.Bool()) case reflect.String: return value.String() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if value.Type() == reflect.TypeOf(time.Duration(0)) { return value.Interface().(time.Duration).String() } return strconv.FormatInt(value.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(value.Uint(), 10) case reflect.Float32, reflect.Float64: return strconv.FormatFloat(value.Float(), 'f', -1, 64) default: return "" } } func (conf *Config) updateFromCLI(c *cli.Command, baseFlags []cli.Flag) error { generatedFlagNames := conf.ToCLIFlagNames(baseFlags) for _, flag := range c.Flags { flagName := flag.Names()[0] if !c.IsSet(flagName) { continue } configValue, ok := generatedFlagNames[flagName] if !ok { continue } if configValue.Kind() == reflect.Ptr { configValue.Set(reflect.New(configValue.Type().Elem())) configValue = configValue.Elem() } value := reflect.ValueOf(c.Value(flagName)) if value.CanConvert(configValue.Type()) { configValue.Set(value.Convert(configValue.Type())) } else { return fmt.Errorf("unsupported generated cli flag type for config: %s (expected %s, got %s)", flagName, configValue.Type(), value.Type()) } } 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.UnmarshalString(c.String("node-ip")) } if c.IsSet("udp-port") { conf.RTC.UDPPort.UnmarshalString(c.String("udp-port")) } if c.IsSet("bind") { conf.BindAddresses = c.StringSlice("bind") } return nil } func (conf *Config) unmarshalKeys(keys string) error { temp := make(map[string]any) 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 } // Note: only pass in logr.Logger with default depth func SetLogger(l logger.Logger) { logger.SetLogger(l, "livekit") } func InitLoggerFromConfig(config *LoggingConfig) { logger.InitFromConfig(&config.Config, "livekit") }