feat: add support for reading configuration from YAML file

This commit is contained in:
zarazaex69
2026-05-13 17:17:31 +03:00
parent 359a2d94df
commit cf6490b5e0
6 changed files with 119 additions and 355 deletions

View File

@@ -1,11 +1,14 @@
// Package main provides the olcrtc CLI entrypoint. // Package main provides the olcrtc CLI entrypoint.
//
// Usage: olcrtc <config.yaml>
//
// All runtime settings come from the YAML file. There are no other CLI flags.
package main package main
import ( import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"flag"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -26,8 +29,11 @@ import (
const modeGen = "gen" const modeGen = "gen"
// ErrDataDirRequired is returned when no data directory is specified. // ErrConfigPathRequired is returned when no config file is provided.
var ErrDataDirRequired = errors.New("data directory required (use -data data)") var ErrConfigPathRequired = errors.New("usage: olcrtc <config.yaml>")
// ErrDataDirRequired is returned when the YAML config does not specify a data directory.
var ErrDataDirRequired = errors.New("data directory required (set 'data:' in YAML)")
//nolint:gochecknoglobals // Tests replace the long-running session runner with a bounded function. //nolint:gochecknoglobals // Tests replace the long-running session runner with a bounded function.
var runSession = session.Run var runSession = session.Run
@@ -35,45 +41,12 @@ var runSession = session.Run
//nolint:gochecknoglobals // Tests replace gen runner with a stub. //nolint:gochecknoglobals // Tests replace gen runner with a stub.
var runGen = execGen var runGen = execGen
type config struct { // loadedConfig bundles the parsed YAML file and the derived session config.
configPath string type loadedConfig struct {
mode string scfg session.Config
link string dataDir string
transport string debug bool
auth string ffmpegPath string
engine string
url string
token string
roomID string
clientID string
socksPort int
socksHost string
socksUser string
socksPass string
keyHex string
debug bool
dataDir string
dnsServer string
socksProxyAddr string
socksProxyPort int
videoWidth int
videoHeight int
videoFPS int
videoBitrate string
videoHW string
videoQRSize int
videoQRRecovery string
videoCodec string
videoTileModule int
videoTileRS int
vp8FPS int
vp8BatchSize int
seiFPS int
seiBatchSize int
seiFragmentSize int
seiAckTimeoutMS int
amount int
ffmpegPath string
} }
func main() { func main() {
@@ -90,76 +63,54 @@ func run() error {
func runWithArgs(args []string) error { func runWithArgs(args []string) error {
session.RegisterDefaults() session.RegisterDefaults()
cfg, err := parseFlagsFrom(args, flag.ExitOnError) if len(args) != 1 || args[0] == "-h" || args[0] == "--help" || args[0] == "-help" {
return ErrConfigPathRequired
}
cfg, err := loadConfig(args[0])
if err != nil { if err != nil {
return err return err
} }
return runWithConfig(cfg) return runWithConfig(cfg)
} }
// applyConfigFile loads cfg.configPath (if set) and merges its values into scfg. func loadConfig(path string) (loadedConfig, error) {
// CLI flags (already populated) take precedence over YAML. f, err := configpkg.Load(path)
func applyConfigFile(cfg config, scfg session.Config) (session.Config, error) {
if cfg.configPath == "" {
return scfg, nil
}
f, err := configpkg.Load(cfg.configPath)
if err != nil { if err != nil {
return scfg, fmt.Errorf("load config: %w", err) return loadedConfig{}, fmt.Errorf("load config: %w", err)
} }
return configpkg.Apply(scfg, f), nil return loadedConfig{
scfg: configpkg.Apply(session.Config{}, f),
dataDir: f.Data,
debug: f.Debug,
ffmpegPath: f.FFmpeg,
}, nil
} }
// mergeFileMeta fills cmd-level fields (data dir, debug, ffmpeg) that aren't func runWithConfig(cfg loadedConfig) error {
// part of session.Config but still need to come from the YAML file.
func mergeFileMeta(cfg *config, f configpkg.File) {
if cfg.dataDir == "" {
cfg.dataDir = f.Data
}
if !cfg.debug {
cfg.debug = f.Debug
}
if (cfg.ffmpegPath == "" || cfg.ffmpegPath == "ffmpeg") && f.FFmpeg != "" {
cfg.ffmpegPath = f.FFmpeg
}
}
func runWithConfig(cfg config) error {
if cfg.configPath != "" {
f, err := configpkg.Load(cfg.configPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
mergeFileMeta(&cfg, f)
}
configureLogging(cfg.debug) configureLogging(cfg.debug)
if cfg.ffmpegPath != "ffmpeg" && cfg.ffmpegPath != "" { if cfg.ffmpegPath != "ffmpeg" && cfg.ffmpegPath != "" {
videochannel.FFmpegPath = cfg.ffmpegPath videochannel.FFmpegPath = cfg.ffmpegPath
} }
if cfg.mode == modeGen { scfg, err := session.ApplyAuthDefaults(cfg.scfg)
return runGen(cfg)
}
return runSessionMode(cfg)
}
func runSessionMode(cfg config) error {
scfg, err := applyConfigFile(cfg, toSessionConfig(cfg))
if err != nil {
return err
}
scfg, err = session.ApplyAuthDefaults(scfg)
if err != nil { if err != nil {
return fmt.Errorf("validate config: %w", err) return fmt.Errorf("validate config: %w", err)
} }
if scfg.Mode == modeGen {
return runGen(scfg)
}
return runSessionMode(cfg.dataDir, scfg)
}
func runSessionMode(dataDir string, scfg session.Config) error {
if err := session.Validate(scfg); err != nil { if err := session.Validate(scfg); err != nil {
return fmt.Errorf("validate config: %w", err) return fmt.Errorf("validate config: %w", err)
} }
dataDir := cfg.dataDir
if dataDir == "" { if dataDir == "" {
return ErrDataDirRequired return ErrDataDirRequired
} }
@@ -194,15 +145,7 @@ func runSessionMode(cfg config) error {
} }
} }
func execGen(cfg config) error { func execGen(scfg session.Config) error {
scfg, err := applyConfigFile(cfg, toSessionConfig(cfg))
if err != nil {
return err
}
scfg, err = session.ApplyAuthDefaults(scfg)
if err != nil {
return fmt.Errorf("validate gen config: %w", err)
}
if err := session.ValidateGen(scfg); err != nil { if err := session.ValidateGen(scfg); err != nil {
return fmt.Errorf("validate gen config: %w", err) return fmt.Errorf("validate gen config: %w", err)
} }
@@ -227,62 +170,6 @@ func execGen(cfg config) error {
} }
} }
func parseFlagsFrom(args []string, errorHandling flag.ErrorHandling) (config, error) {
cfg := config{}
fs := flag.NewFlagSet("olcrtc", errorHandling)
if errorHandling == flag.ContinueOnError {
fs.SetOutput(io.Discard)
}
fs.StringVar(&cfg.configPath, "config", "", "Path to YAML config file (CLI flags override file values)")
fs.StringVar(&cfg.mode, "mode", "", "Mode: srv or cnc")
fs.StringVar(&cfg.link, "link", "", "Link: direct (p2p connection type)")
fs.StringVar(&cfg.transport, "transport", "", "Transport: datachannel, videochannel, seichannel")
fs.StringVar(&cfg.auth, "auth", "", "Auth provider: telemost, jazz, wbstream, none")
fs.StringVar(&cfg.engine, "engine", "", "Engine (required when -auth none): livekit, goolom, salutejazz")
fs.StringVar(&cfg.url, "url", "", "SFU WebSocket URL (required when -auth none)")
fs.StringVar(&cfg.token, "token", "", "Access token (required when -auth none)")
fs.StringVar(&cfg.roomID, "id", "", "Room ID")
fs.StringVar(&cfg.clientID, "client-id", "", "Client ID: binds one srv to one cnc (required)")
fs.IntVar(&cfg.socksPort, "socks-port", 0, "SOCKS5 port (client only)")
fs.StringVar(&cfg.socksHost, "socks-host", "", "SOCKS5 listen host (client only)")
fs.StringVar(&cfg.socksUser, "socks-user", "", "SOCKS5 username for incoming connections (client only, optional)")
fs.StringVar(&cfg.socksPass, "socks-pass", "", "SOCKS5 password for incoming connections (client only, optional)")
fs.StringVar(&cfg.keyHex, "key", "", "Shared encryption key (hex)")
fs.BoolVar(&cfg.debug, "debug", false, "Enable verbose logging")
fs.StringVar(&cfg.dataDir, "data", "", "Path to data directory")
fs.StringVar(&cfg.dnsServer, "dns", "", "DNS server (e.g. 1.1.1.1:53)")
fs.StringVar(&cfg.socksProxyAddr, "socks-proxy", "", "SOCKS5 proxy address (server only)")
fs.IntVar(&cfg.socksProxyPort, "socks-proxy-port", 0, "SOCKS5 proxy port (server only)")
fs.IntVar(&cfg.videoWidth, "video-w", 0, "Video logical width (videochannel only)")
fs.IntVar(&cfg.videoHeight, "video-h", 0, "Video logical height (videochannel only)")
fs.IntVar(&cfg.videoFPS, "video-fps", 0, "Video frames per second (videochannel only)")
fs.StringVar(&cfg.videoBitrate, "video-bitrate", "", "Video bitrate (videochannel only)")
fs.StringVar(&cfg.videoHW, "video-hw", "", "Hardware acceleration (none, nvenc)")
fs.IntVar(&cfg.videoQRSize, "video-qr-size", 0, "Video QR code fragment size (videochannel only)")
fs.StringVar(&cfg.videoQRRecovery, "video-qr-recovery", "low",
"QR error correction: low (7%), medium (15%), high (25%), highest (30%)")
fs.StringVar(&cfg.videoCodec, "video-codec", "qrcode", "Visual codec: qrcode or tile")
fs.IntVar(&cfg.videoTileModule, "video-tile-module", 0,
"Tile module size in pixels 1..270 (videochannel tile only, default 4)")
fs.IntVar(&cfg.videoTileRS, "video-tile-rs", 0,
"Tile Reed-Solomon parity percent 0..200 (videochannel tile only, default 20)")
fs.IntVar(&cfg.vp8FPS, "vp8-fps", 0, "VP8 frames per second (vp8channel only, default 25)")
fs.IntVar(&cfg.vp8BatchSize, "vp8-batch", 0, "VP8 frames per tick (vp8channel only, default 1)")
fs.IntVar(&cfg.seiFPS, "fps", 0, "Frames per second for transports that use video timing (seichannel)")
fs.IntVar(&cfg.seiBatchSize, "batch", 0, "Transport frames per tick for batched transports (seichannel)")
fs.IntVar(&cfg.seiFragmentSize, "frag", 0, "Fragment size in bytes for fragmented transports (seichannel)")
fs.IntVar(&cfg.seiAckTimeoutMS, "ack-ms", 0, "ACK timeout in milliseconds for reliable visual transports (seichannel)")
fs.IntVar(&cfg.amount, "amount", 0, "Number of rooms to generate (gen mode only)")
fs.StringVar(&cfg.ffmpegPath, "ffmpeg", "ffmpeg", "Path to ffmpeg executable")
if err := fs.Parse(args); err != nil {
return cfg, fmt.Errorf("parse flags: %w", err)
}
return cfg, nil
}
// noisyPrefixes lists log prefixes from third-party libs that spam via std log. // noisyPrefixes lists log prefixes from third-party libs that spam via std log.
var noisyPrefixes = [][]byte{ //nolint:gochecknoglobals // package-level filter list var noisyPrefixes = [][]byte{ //nolint:gochecknoglobals // package-level filter list
[]byte("turnc"), []byte("[turn]"), []byte("Fail to refresh permissions"), []byte("turnc"), []byte("[turn]"), []byte("Fail to refresh permissions"),
@@ -309,10 +196,8 @@ func configureLogging(debug bool) {
logger.SetVerbose(true) logger.SetVerbose(true)
return return
} }
// Suppress noisy LiveKit/pion logs unless debug is enabled.
_ = os.Setenv("PION_LOG_DISABLE", "all") _ = os.Setenv("PION_LOG_DISABLE", "all")
lksdk.SetLogger(protoLogger.GetDiscardLogger()) lksdk.SetLogger(protoLogger.GetDiscardLogger())
// turnc logs via std log directly — filter it out.
log.SetOutput(filteredWriter{w: os.Stderr}) log.SetOutput(filteredWriter{w: os.Stderr})
} }
@@ -339,45 +224,6 @@ func loadNames(dataDir string) error {
return nil return nil
} }
func toSessionConfig(cfg config) session.Config {
return session.Config{
Mode: cfg.mode,
Link: cfg.link,
Transport: cfg.transport,
Auth: cfg.auth,
Engine: cfg.engine,
URL: cfg.url,
Token: cfg.token,
RoomID: cfg.roomID,
ClientID: cfg.clientID,
KeyHex: cfg.keyHex,
SOCKSHost: cfg.socksHost,
SOCKSPort: cfg.socksPort,
SOCKSUser: cfg.socksUser,
SOCKSPass: cfg.socksPass,
DNSServer: cfg.dnsServer,
SOCKSProxyAddr: cfg.socksProxyAddr,
SOCKSProxyPort: cfg.socksProxyPort,
VideoWidth: cfg.videoWidth,
VideoHeight: cfg.videoHeight,
VideoFPS: cfg.videoFPS,
VideoBitrate: cfg.videoBitrate,
VideoHW: cfg.videoHW,
VideoQRSize: cfg.videoQRSize,
VideoQRRecovery: cfg.videoQRRecovery,
VideoCodec: cfg.videoCodec,
VideoTileModule: cfg.videoTileModule,
VideoTileRS: cfg.videoTileRS,
VP8FPS: cfg.vp8FPS,
VP8BatchSize: cfg.vp8BatchSize,
SEIFPS: cfg.seiFPS,
SEIBatchSize: cfg.seiBatchSize,
SEIFragmentSize: cfg.seiFragmentSize,
SEIAckTimeoutMS: cfg.seiAckTimeoutMS,
Amount: cfg.amount,
}
}
func waitForShutdown(errCh <-chan error) error { func waitForShutdown(errCh <-chan error) error {
done := make(chan error, 1) done := make(chan error, 1)
go func() { go func() {

View File

@@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"errors" "errors"
"flag"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -14,110 +13,40 @@ import (
var errBoom = errors.New("boom") var errBoom = errors.New("boom")
//nolint:cyclop // table-driven test naturally has many branches func writeYAML(t *testing.T, body string) string {
func TestToSessionConfig(t *testing.T) { t.Helper()
cfg := config{ dir := t.TempDir()
mode: "cnc", path := filepath.Join(dir, "olcrtc.yaml")
link: "direct", //nolint:goconst // test literal, repetition is intentional if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
transport: "vp8channel", t.Fatalf("write yaml: %v", err)
auth: "jazz", //nolint:goconst // test literal, repetition is intentional
roomID: "room", //nolint:goconst // test literal, repetition is intentional
clientID: "client", //nolint:goconst // test literal, repetition is intentional
keyHex: "key", //nolint:goconst // test literal, repetition is intentional
socksHost: "127.0.0.1",
socksPort: 1080,
dnsServer: "1.1.1.1:53", //nolint:goconst // test literal, repetition is intentional
socksProxyAddr: "proxy",
socksProxyPort: 1081,
videoWidth: 640,
videoHeight: 480,
videoFPS: 30,
videoBitrate: "1M",
videoHW: "none",
videoQRSize: 4,
videoQRRecovery: "low",
videoCodec: "qrcode",
videoTileModule: 4,
videoTileRS: 20,
vp8FPS: 25,
vp8BatchSize: 8,
seiFPS: 40,
seiBatchSize: 3,
seiFragmentSize: 512,
seiAckTimeoutMS: 1500,
amount: 5,
}
got := toSessionConfig(cfg)
if got.Mode != cfg.mode || got.Auth != "jazz" || got.SOCKSPort != cfg.socksPort ||
got.VideoTileRS != cfg.videoTileRS || got.VP8BatchSize != cfg.vp8BatchSize ||
got.SEIFPS != cfg.seiFPS || got.SEIBatchSize != cfg.seiBatchSize ||
got.SEIFragmentSize != cfg.seiFragmentSize || got.SEIAckTimeoutMS != cfg.seiAckTimeoutMS ||
got.Amount != cfg.amount {
t.Fatalf("toSessionConfig() = %+v", got)
} }
return path
} }
//nolint:cyclop // table-driven test naturally has many branches func TestRunWithArgsRequiresConfig(t *testing.T) {
func TestParseFlagsFrom(t *testing.T) { session.RegisterDefaults()
cfg, err := parseFlagsFrom([]string{ if err := runWithArgs(nil); !errors.Is(err, ErrConfigPathRequired) {
"-mode", "srv", //nolint:goconst // test literal, repetition is intentional t.Fatalf("runWithArgs(nil) = %v, want %v", err, ErrConfigPathRequired)
"-link", "direct",
"-transport", "vp8channel",
"-auth", "telemost",
"-id", "room",
"-client-id", "client",
"-socks-port", "1080",
"-socks-host", "127.0.0.1",
"-key", "key",
"-debug",
"-data", "data",
"-dns", "9.9.9.9:53",
"-socks-proxy", "proxy",
"-socks-proxy-port", "1081",
"-video-w", "640",
"-video-h", "480",
"-video-fps", "30",
"-video-bitrate", "1M",
"-video-hw", "none",
"-video-qr-size", "128",
"-video-qr-recovery", "high",
"-video-codec", "tile",
"-video-tile-module", "6",
"-video-tile-rs", "40",
"-vp8-fps", "24",
"-vp8-batch", "3",
"-fps", "40",
"-batch", "4",
"-frag", "512",
"-ack-ms", "1500",
"-amount", "7",
}, flag.ContinueOnError)
if err != nil {
t.Fatalf("parseFlagsFrom() error = %v", err)
} }
if cfg.mode != "srv" || cfg.auth != "telemost" || cfg.roomID != "room" || if err := runWithArgs([]string{"-h"}); !errors.Is(err, ErrConfigPathRequired) {
cfg.debug != true || cfg.videoCodec != "tile" || cfg.videoTileRS != 40 || t.Fatalf("runWithArgs(-h) = %v, want %v", err, ErrConfigPathRequired)
cfg.vp8FPS != 24 || cfg.vp8BatchSize != 3 || cfg.seiFPS != 40 ||
cfg.seiBatchSize != 4 || cfg.seiFragmentSize != 512 || cfg.seiAckTimeoutMS != 1500 ||
cfg.amount != 7 {
t.Fatalf("parseFlagsFrom() = %+v", cfg)
} }
if err := runWithArgs([]string{"a.yaml", "b.yaml"}); !errors.Is(err, ErrConfigPathRequired) {
_, err = parseFlagsFrom([]string{"-bad"}, flag.ContinueOnError) t.Fatalf("runWithArgs(two args) = %v, want %v", err, ErrConfigPathRequired)
if err == nil {
t.Fatal("parseFlagsFrom(bad flag) error = nil")
} }
} }
func TestRunGenModeValidationErrors(t *testing.T) { func TestRunGenModeValidationErrors(t *testing.T) {
session.RegisterDefaults() session.RegisterDefaults()
if err := runWithConfig(config{mode: "gen"}); err == nil { //nolint:goconst // test literal, repetition is intentional if err := runWithConfig(loadedConfig{scfg: session.Config{Mode: "gen"}}); err == nil {
t.Fatal("runWithConfig(gen, no carrier) error = nil") t.Fatal("runWithConfig(gen, no carrier) error = nil")
} }
if err := runWithConfig(config{mode: "gen", auth: "wbstream", dnsServer: "1.1.1.1:53"}); err == nil { //nolint:goconst,lll // test literal, repetition is intentional cfg := loadedConfig{scfg: session.Config{
Mode: "gen", Auth: "wbstream", DNSServer: "1.1.1.1:53",
}}
if err := runWithConfig(cfg); err == nil {
t.Fatal("runWithConfig(gen, amount=0) error = nil") t.Fatal("runWithConfig(gen, amount=0) error = nil")
} }
} }
@@ -128,16 +57,18 @@ func TestRunGenModeCallsGen(t *testing.T) {
var collected []string var collected []string
oldRunGen := runGen oldRunGen := runGen
t.Cleanup(func() { runGen = oldRunGen }) t.Cleanup(func() { runGen = oldRunGen })
runGen = func(cfg config) error { runGen = func(scfg session.Config) error {
if cfg.auth != "wbstream" || cfg.dnsServer != "1.1.1.1:53" || cfg.amount != 3 { if scfg.Auth != "wbstream" || scfg.DNSServer != "1.1.1.1:53" || scfg.Amount != 3 {
t.Fatalf("runGen cfg = %+v", cfg) t.Fatalf("runGen scfg = %+v", scfg)
} }
collected = append(collected, "ok") collected = append(collected, "ok")
return nil return nil
} }
err := runWithConfig(config{mode: "gen", auth: "wbstream", dnsServer: "1.1.1.1:53", amount: 3}) cfg := loadedConfig{scfg: session.Config{
if err != nil { Mode: "gen", Auth: "wbstream", DNSServer: "1.1.1.1:53", Amount: 3,
}}
if err := runWithConfig(cfg); err != nil {
t.Fatalf("runWithConfig(gen) error = %v", err) t.Fatalf("runWithConfig(gen) error = %v", err)
} }
if len(collected) != 1 { if len(collected) != 1 {
@@ -147,22 +78,21 @@ func TestRunGenModeCallsGen(t *testing.T) {
func TestRunWithConfigValidationAndDataDirErrors(t *testing.T) { func TestRunWithConfigValidationAndDataDirErrors(t *testing.T) {
session.RegisterDefaults() session.RegisterDefaults()
cfg := config{ scfg := session.Config{
mode: "srv", Mode: "srv",
link: "direct", Link: "direct",
transport: "datachannel", Transport: "datachannel",
auth: "jazz", Auth: "jazz",
clientID: "client", ClientID: "client",
keyHex: "key", KeyHex: "key",
dnsServer: "1.1.1.1:53", DNSServer: "1.1.1.1:53",
videoCodec: "qrcode",
} }
if err := runWithConfig(cfg); !errors.Is(err, ErrDataDirRequired) { if err := runWithConfig(loadedConfig{scfg: scfg}); !errors.Is(err, ErrDataDirRequired) {
t.Fatalf("runWithConfig(no data dir) = %v, want %v", err, ErrDataDirRequired) t.Fatalf("runWithConfig(no data dir) = %v, want %v", err, ErrDataDirRequired)
} }
cfg.mode = "" scfg.Mode = ""
if err := runWithConfig(cfg); err == nil { if err := runWithConfig(loadedConfig{scfg: scfg}); err == nil {
t.Fatal("runWithConfig(invalid config) error = nil") t.Fatal("runWithConfig(invalid config) error = nil")
} }
} }
@@ -194,17 +124,22 @@ func TestRunWithArgsSuccessfulSessionReturn(t *testing.T) {
return nil return nil
} }
err := runWithArgs([]string{ yamlPath := writeYAML(t, `
"-mode", "srv", mode: srv
"-link", "direct", link: direct
"-transport", "datachannel", auth:
"-auth", "jazz", provider: jazz
"-client-id", "client", room:
"-key", "key", client_id: client
"-dns", "1.1.1.1:53", crypto:
"-data", dir, key: key
}) net:
if err != nil { transport: datachannel
dns: 1.1.1.1:53
data: `+dir+`
`)
if err := runWithArgs([]string{yamlPath}); err != nil {
t.Fatalf("runWithArgs() error = %v", err) t.Fatalf("runWithArgs() error = %v", err)
} }
if !called { if !called {

View File

@@ -5,7 +5,6 @@
mode: cnc mode: cnc
link: direct link: direct
carrier: ""
auth: auth:
provider: wbstream # must match the server provider: wbstream # must match the server

View File

@@ -1,17 +1,10 @@
# Configuration # Configuration
olcrtc accepts the same settings via CLI flags or a YAML file. Use whichever olcrtc reads its entire runtime configuration from a single YAML file.
fits your deployment: There are no other CLI flags.
```bash ```bash
# CLI flags (existing behaviour) olcrtc /etc/olcrtc/server.yaml
olcrtc -mode srv -auth wbstream -id room123 -key $(openssl rand -hex 32) ...
# YAML file
olcrtc -config /etc/olcrtc/server.yaml
# YAML file plus CLI overrides — any flag wins over the corresponding YAML field
olcrtc -config /etc/olcrtc/server.yaml -id room999
``` ```
Examples: Examples:
@@ -21,31 +14,24 @@ Examples:
## Schema ## Schema
| YAML path | CLI flag | Notes | | YAML path | Notes |
|----------------------------|----------------------|-----------------------------------------------| |------------------------------------------------------------------|-----------------------------------------------------------|
| `mode` | `-mode` | `srv`, `cnc`, or `gen` | | `mode` | `srv`, `cnc`, or `gen` |
| `link` | `-link` | `direct` | | `link` | `direct` |
| `auth.provider` | `-auth` | `telemost`, `jazz`, `wbstream`, `none` | | `auth.provider` | `telemost`, `jazz`, `wbstream`, `none` |
| `room.id` | `-id` | conference room id | | `room.id` | conference room id |
| `room.client_id` | `-client-id` | deprecated, will be removed | | `room.client_id` | deprecated, will be removed |
| `crypto.key` | `-key` | 64-char hex (32 bytes) | | `crypto.key` | 64-char hex (32 bytes) |
| `net.transport` | `-transport` | `datachannel`, `videochannel`, `seichannel`, `vp8channel` | | `net.transport` | `datachannel`, `videochannel`, `seichannel`, `vp8channel` |
| `net.dns` | `-dns` | resolver `host:port` | | `net.dns` | resolver `host:port` |
| `socks.host` / `.port` | `-socks-host` / `-socks-port` | client-side listener | | `socks.host` / `.port` | client-side listener |
| `socks.user` / `.pass` | `-socks-user` / `-socks-pass` | optional client-side auth | | `socks.user` / `.pass` | optional client-side auth |
| `socks.proxy_addr` / `.proxy_port` | `-socks-proxy` / `-socks-proxy-port` | server-side egress proxy | | `socks.proxy_addr` / `.proxy_port` | server-side egress proxy |
| `engine.name` / `.url` / `.token` | `-engine` / `-url` / `-token` | only when `auth.provider: none` | | `engine.name` / `.url` / `.token` | only when `auth.provider: none` |
| `video.*` | `-video-*` | videochannel tuning | | `video.*` | videochannel tuning |
| `vp8.*` | `-vp8-*` | vp8channel tuning | | `vp8.*` | vp8channel tuning |
| `sei.fps` / `.batch_size` / `.fragment_size` / `.ack_timeout_ms` | `-fps` / `-batch` / `-frag` / `-ack-ms` | seichannel tuning | | `sei.fps` / `.batch_size` / `.fragment_size` / `.ack_timeout_ms` | seichannel tuning |
| `gen.amount` | `-amount` | gen mode: number of rooms to create | | `gen.amount` | gen mode: number of rooms to create |
| `data` | `-data` | path to data directory | | `data` | path to data directory |
| `debug` | `-debug` | verbose logging | | `debug` | verbose logging |
| `ffmpeg` | `-ffmpeg` | path to ffmpeg binary | | `ffmpeg` | path to ffmpeg binary |
## Precedence
`CLI flag (non-zero) > YAML value > zero value`.
A CLI flag with its zero value (e.g. `-socks-port 0`) does NOT override a YAML
value — pass an explicit non-zero value to override.

View File

@@ -6,7 +6,6 @@ mode: srv
# Connection topology # Connection topology
link: direct # p2p link type link: direct # p2p link type
carrier: "" # leave empty for default selection from auth provider
auth: auth:
provider: wbstream # telemost | jazz | wbstream | none provider: wbstream # telemost | jazz | wbstream | none

View File

@@ -23,7 +23,6 @@ var ErrConfigNotFound = errors.New("config file not found")
type File struct { type File struct {
Mode string `yaml:"mode"` Mode string `yaml:"mode"`
Link string `yaml:"link"` Link string `yaml:"link"`
Carrier string `yaml:"carrier"`
Auth Auth `yaml:"auth"` Auth Auth `yaml:"auth"`
Room Room `yaml:"room"` Room Room `yaml:"room"`
Crypto Crypto `yaml:"crypto"` Crypto Crypto `yaml:"crypto"`