mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-05-26 07:08:11 +00:00
feat: add support for reading configuration from YAML file
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
Reference in New Issue
Block a user