mirror of
https://github.com/openlibrecommunity/olcrtc.git
synced 2026-05-26 07:08:11 +00:00
feat: add YAML configuration support
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
|||||||
protoLogger "github.com/livekit/protocol/logger"
|
protoLogger "github.com/livekit/protocol/logger"
|
||||||
lksdk "github.com/livekit/server-sdk-go/v2"
|
lksdk "github.com/livekit/server-sdk-go/v2"
|
||||||
"github.com/openlibrecommunity/olcrtc/internal/app/session"
|
"github.com/openlibrecommunity/olcrtc/internal/app/session"
|
||||||
|
configpkg "github.com/openlibrecommunity/olcrtc/internal/config"
|
||||||
"github.com/openlibrecommunity/olcrtc/internal/logger"
|
"github.com/openlibrecommunity/olcrtc/internal/logger"
|
||||||
"github.com/openlibrecommunity/olcrtc/internal/names"
|
"github.com/openlibrecommunity/olcrtc/internal/names"
|
||||||
"github.com/openlibrecommunity/olcrtc/internal/transport/videochannel"
|
"github.com/openlibrecommunity/olcrtc/internal/transport/videochannel"
|
||||||
@@ -35,6 +36,7 @@ var runSession = session.Run
|
|||||||
var runGen = execGen
|
var runGen = execGen
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
configPath string
|
||||||
mode string
|
mode string
|
||||||
link string
|
link string
|
||||||
transport string
|
transport string
|
||||||
@@ -95,7 +97,42 @@ func runWithArgs(args []string) error {
|
|||||||
return runWithConfig(cfg)
|
return runWithConfig(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyConfigFile loads cfg.configPath (if set) and merges its values into scfg.
|
||||||
|
// CLI flags (already populated) take precedence over YAML.
|
||||||
|
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 {
|
||||||
|
return scfg, fmt.Errorf("load config: %w", err)
|
||||||
|
}
|
||||||
|
return configpkg.Apply(scfg, f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeFileMeta fills cmd-level fields (data dir, debug, ffmpeg) that aren't
|
||||||
|
// 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 {
|
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 != "" {
|
||||||
@@ -110,7 +147,11 @@ func runWithConfig(cfg config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runSessionMode(cfg config) error {
|
func runSessionMode(cfg config) error {
|
||||||
scfg, err := session.ApplyAuthDefaults(toSessionConfig(cfg))
|
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)
|
||||||
}
|
}
|
||||||
@@ -118,16 +159,17 @@ func runSessionMode(cfg config) error {
|
|||||||
return fmt.Errorf("validate config: %w", err)
|
return fmt.Errorf("validate config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.dataDir == "" {
|
dataDir := cfg.dataDir
|
||||||
|
if dataDir == "" {
|
||||||
return ErrDataDirRequired
|
return ErrDataDirRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
dataDir, err := resolveDataDir(cfg.dataDir)
|
resolvedDataDir, err := resolveDataDir(dataDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := loadNames(dataDir); err != nil {
|
if err := loadNames(resolvedDataDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +195,11 @@ func runSessionMode(cfg config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func execGen(cfg config) error {
|
func execGen(cfg config) error {
|
||||||
scfg, err := session.ApplyAuthDefaults(toSessionConfig(cfg))
|
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 gen config: %w", err)
|
return fmt.Errorf("validate gen config: %w", err)
|
||||||
}
|
}
|
||||||
@@ -188,6 +234,7 @@ func parseFlagsFrom(args []string, errorHandling flag.ErrorHandling) (config, er
|
|||||||
fs.SetOutput(io.Discard)
|
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.mode, "mode", "", "Mode: srv or cnc")
|
||||||
fs.StringVar(&cfg.link, "link", "", "Link: direct (p2p connection type)")
|
fs.StringVar(&cfg.link, "link", "", "Link: direct (p2p connection type)")
|
||||||
fs.StringVar(&cfg.transport, "transport", "", "Transport: datachannel, videochannel, seichannel")
|
fs.StringVar(&cfg.transport, "transport", "", "Transport: datachannel, videochannel, seichannel")
|
||||||
|
|||||||
60
docs/client.example.yaml
Normal file
60
docs/client.example.yaml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# olcrtc client config example
|
||||||
|
# Run with: olcrtc -config client.yaml
|
||||||
|
# Any CLI flag (e.g. -key, -id) overrides the corresponding YAML field.
|
||||||
|
|
||||||
|
mode: cnc
|
||||||
|
|
||||||
|
link: direct
|
||||||
|
carrier: ""
|
||||||
|
|
||||||
|
auth:
|
||||||
|
provider: wbstream # must match the server
|
||||||
|
|
||||||
|
room:
|
||||||
|
id: "ROOM_ID_HERE" # must match the server
|
||||||
|
client_id: "default" # must match the server (deprecated)
|
||||||
|
|
||||||
|
crypto:
|
||||||
|
key: "REPLACE_ME_WITH_64_HEX_CHARS" # must match the server
|
||||||
|
|
||||||
|
net:
|
||||||
|
transport: datachannel # must match the server
|
||||||
|
dns: "8.8.8.8:53"
|
||||||
|
|
||||||
|
# Local SOCKS5 listener exposed to applications
|
||||||
|
socks:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 8808
|
||||||
|
user: "" # optional inbound auth
|
||||||
|
pass: ""
|
||||||
|
|
||||||
|
# Direct engine mode — only when auth.provider is "none"
|
||||||
|
engine:
|
||||||
|
name: ""
|
||||||
|
url: ""
|
||||||
|
token: ""
|
||||||
|
|
||||||
|
vp8:
|
||||||
|
fps: 25
|
||||||
|
batch_size: 1
|
||||||
|
|
||||||
|
sei:
|
||||||
|
fps: 20
|
||||||
|
batch_size: 1
|
||||||
|
fragment_size: 900
|
||||||
|
ack_timeout_ms: 3000
|
||||||
|
|
||||||
|
video:
|
||||||
|
width: 1920
|
||||||
|
height: 1080
|
||||||
|
fps: 30
|
||||||
|
bitrate: "2M"
|
||||||
|
hw: none
|
||||||
|
codec: qrcode
|
||||||
|
qr_size: 0
|
||||||
|
qr_recovery: low
|
||||||
|
tile_module: 4
|
||||||
|
tile_rs: 20
|
||||||
|
|
||||||
|
data: data
|
||||||
|
debug: false
|
||||||
51
docs/configuration.md
Normal file
51
docs/configuration.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
olcrtc accepts the same settings via CLI flags or a YAML file. Use whichever
|
||||||
|
fits your deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CLI flags (existing behaviour)
|
||||||
|
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:
|
||||||
|
|
||||||
|
- [`server.example.yaml`](./server.example.yaml)
|
||||||
|
- [`client.example.yaml`](./client.example.yaml)
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
| YAML path | CLI flag | Notes |
|
||||||
|
|----------------------------|----------------------|-----------------------------------------------|
|
||||||
|
| `mode` | `-mode` | `srv`, `cnc`, or `gen` |
|
||||||
|
| `link` | `-link` | `direct` |
|
||||||
|
| `auth.provider` | `-auth` | `telemost`, `jazz`, `wbstream`, `none` |
|
||||||
|
| `room.id` | `-id` | conference room id |
|
||||||
|
| `room.client_id` | `-client-id` | deprecated, will be removed |
|
||||||
|
| `crypto.key` | `-key` | 64-char hex (32 bytes) |
|
||||||
|
| `net.transport` | `-transport` | `datachannel`, `videochannel`, `seichannel`, `vp8channel` |
|
||||||
|
| `net.dns` | `-dns` | resolver `host:port` |
|
||||||
|
| `socks.host` / `.port` | `-socks-host` / `-socks-port` | client-side listener |
|
||||||
|
| `socks.user` / `.pass` | `-socks-user` / `-socks-pass` | optional client-side auth |
|
||||||
|
| `socks.proxy_addr` / `.proxy_port` | `-socks-proxy` / `-socks-proxy-port` | server-side egress proxy |
|
||||||
|
| `engine.name` / `.url` / `.token` | `-engine` / `-url` / `-token` | only when `auth.provider: none` |
|
||||||
|
| `video.*` | `-video-*` | videochannel tuning |
|
||||||
|
| `vp8.*` | `-vp8-*` | vp8channel tuning |
|
||||||
|
| `sei.fps` / `.batch_size` / `.fragment_size` / `.ack_timeout_ms` | `-fps` / `-batch` / `-frag` / `-ack-ms` | seichannel tuning |
|
||||||
|
| `gen.amount` | `-amount` | gen mode: number of rooms to create |
|
||||||
|
| `data` | `-data` | path to data directory |
|
||||||
|
| `debug` | `-debug` | verbose logging |
|
||||||
|
| `ffmpeg` | `-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.
|
||||||
64
docs/server.example.yaml
Normal file
64
docs/server.example.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# olcrtc server config example
|
||||||
|
# Run with: olcrtc -config server.yaml
|
||||||
|
# Any CLI flag (e.g. -key, -id) overrides the corresponding YAML field.
|
||||||
|
|
||||||
|
mode: srv
|
||||||
|
|
||||||
|
# Connection topology
|
||||||
|
link: direct # p2p link type
|
||||||
|
carrier: "" # leave empty for default selection from auth provider
|
||||||
|
|
||||||
|
auth:
|
||||||
|
provider: wbstream # telemost | jazz | wbstream | none
|
||||||
|
|
||||||
|
room:
|
||||||
|
id: "ROOM_ID_HERE"
|
||||||
|
client_id: "default" # deprecated: server identifier (will be removed in upcoming refactor)
|
||||||
|
|
||||||
|
crypto:
|
||||||
|
# 32-byte hex (64 chars). Generate with: openssl rand -hex 32
|
||||||
|
key: "REPLACE_ME_WITH_64_HEX_CHARS"
|
||||||
|
|
||||||
|
net:
|
||||||
|
transport: datachannel # datachannel | videochannel | seichannel | vp8channel
|
||||||
|
dns: "8.8.8.8:53"
|
||||||
|
|
||||||
|
# Outbound SOCKS5 proxy for server-side egress (optional)
|
||||||
|
socks:
|
||||||
|
proxy_addr: "" # e.g. "127.0.0.1"
|
||||||
|
proxy_port: 0 # e.g. 1080
|
||||||
|
|
||||||
|
# Direct engine mode — only used when auth.provider is "none"
|
||||||
|
engine:
|
||||||
|
name: "" # livekit | goolom | salutejazz
|
||||||
|
url: ""
|
||||||
|
token: ""
|
||||||
|
|
||||||
|
# vp8channel tuning (only when net.transport == vp8channel)
|
||||||
|
vp8:
|
||||||
|
fps: 25
|
||||||
|
batch_size: 1
|
||||||
|
|
||||||
|
# seichannel tuning (only when net.transport == seichannel)
|
||||||
|
sei:
|
||||||
|
fps: 20
|
||||||
|
batch_size: 1
|
||||||
|
fragment_size: 900
|
||||||
|
ack_timeout_ms: 3000
|
||||||
|
|
||||||
|
# videochannel tuning (only when net.transport == videochannel)
|
||||||
|
video:
|
||||||
|
width: 1920
|
||||||
|
height: 1080
|
||||||
|
fps: 30
|
||||||
|
bitrate: "2M"
|
||||||
|
hw: none # none | nvenc
|
||||||
|
codec: qrcode # qrcode | tile (tile requires 1080x1080)
|
||||||
|
qr_size: 0 # 0 = auto
|
||||||
|
qr_recovery: low # low (7%) | medium (15%) | high (25%) | highest (30%)
|
||||||
|
tile_module: 4 # 1..270, only for codec: tile
|
||||||
|
tile_rs: 20 # 0..200, only for codec: tile
|
||||||
|
|
||||||
|
data: data # data directory (names files etc.)
|
||||||
|
debug: false
|
||||||
|
ffmpeg: ffmpeg # path to ffmpeg binary (only used by videochannel)
|
||||||
2
go.mod
2
go.mod
@@ -17,6 +17,7 @@ require (
|
|||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
|
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
|
||||||
google.golang.org/genproto v0.0.0-20260209200024-4cfbd4190f57
|
google.golang.org/genproto v0.0.0-20260209200024-4cfbd4190f57
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -88,6 +89,5 @@ require (
|
|||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||||
google.golang.org/grpc v1.79.1 // indirect
|
google.golang.org/grpc v1.79.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
182
internal/config/config.go
Normal file
182
internal/config/config.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Package config loads olcrtc runtime configuration from YAML files.
|
||||||
|
//
|
||||||
|
// The YAML schema mirrors [session.Config]. Fields left unset in the file
|
||||||
|
// remain at their zero value, allowing CLI flags to fill them in. Use
|
||||||
|
// [Apply] to merge a parsed [File] onto an existing [session.Config];
|
||||||
|
// non-zero fields in the session config (typically populated from CLI flags)
|
||||||
|
// take precedence over the YAML values.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/openlibrecommunity/olcrtc/internal/app/session"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrConfigNotFound is returned when a config file path is set but the file does not exist.
|
||||||
|
var ErrConfigNotFound = errors.New("config file not found")
|
||||||
|
|
||||||
|
// File is the on-disk YAML schema.
|
||||||
|
type File struct {
|
||||||
|
Mode string `yaml:"mode"`
|
||||||
|
Link string `yaml:"link"`
|
||||||
|
Carrier string `yaml:"carrier"`
|
||||||
|
Auth Auth `yaml:"auth"`
|
||||||
|
Room Room `yaml:"room"`
|
||||||
|
Crypto Crypto `yaml:"crypto"`
|
||||||
|
Net Net `yaml:"net"`
|
||||||
|
SOCKS SOCKS `yaml:"socks"`
|
||||||
|
Engine Engine `yaml:"engine"`
|
||||||
|
Video Video `yaml:"video"`
|
||||||
|
VP8 VP8 `yaml:"vp8"`
|
||||||
|
SEI SEI `yaml:"sei"`
|
||||||
|
Gen Gen `yaml:"gen"`
|
||||||
|
Data string `yaml:"data"`
|
||||||
|
Debug bool `yaml:"debug"`
|
||||||
|
FFmpeg string `yaml:"ffmpeg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth selects the auth provider.
|
||||||
|
type Auth struct {
|
||||||
|
Provider string `yaml:"provider"` // telemost, jazz, wbstream, none
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room identifies the conference room.
|
||||||
|
type Room struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
ClientID string `yaml:"client_id"` // deprecated: server identifier (will be removed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crypto holds the shared secret used to authenticate and encrypt the tunnel.
|
||||||
|
type Crypto struct {
|
||||||
|
Key string `yaml:"key"` // 64-char hex (32 bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Net groups network and transport selection.
|
||||||
|
type Net struct {
|
||||||
|
Transport string `yaml:"transport"` // datachannel, videochannel, seichannel, vp8channel
|
||||||
|
DNS string `yaml:"dns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SOCKS bundles SOCKS5 listener and outbound-proxy settings.
|
||||||
|
type SOCKS struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Pass string `yaml:"pass"`
|
||||||
|
ProxyAddr string `yaml:"proxy_addr"`
|
||||||
|
ProxyPort int `yaml:"proxy_port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engine selects a direct SFU connection when Auth.Provider is "none".
|
||||||
|
type Engine struct {
|
||||||
|
Name string `yaml:"name"` // livekit, goolom, salutejazz
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video tunes the videochannel transport.
|
||||||
|
type Video struct {
|
||||||
|
Width int `yaml:"width"`
|
||||||
|
Height int `yaml:"height"`
|
||||||
|
FPS int `yaml:"fps"`
|
||||||
|
Bitrate string `yaml:"bitrate"`
|
||||||
|
HW string `yaml:"hw"`
|
||||||
|
QRSize int `yaml:"qr_size"`
|
||||||
|
QRRecovery string `yaml:"qr_recovery"`
|
||||||
|
Codec string `yaml:"codec"`
|
||||||
|
TileModule int `yaml:"tile_module"`
|
||||||
|
TileRS int `yaml:"tile_rs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VP8 tunes the vp8channel transport.
|
||||||
|
type VP8 struct {
|
||||||
|
FPS int `yaml:"fps"`
|
||||||
|
BatchSize int `yaml:"batch_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEI tunes the seichannel transport.
|
||||||
|
type SEI struct {
|
||||||
|
FPS int `yaml:"fps"`
|
||||||
|
BatchSize int `yaml:"batch_size"`
|
||||||
|
FragmentSize int `yaml:"fragment_size"`
|
||||||
|
AckTimeoutMS int `yaml:"ack_timeout_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gen controls room-generation mode.
|
||||||
|
type Gen struct {
|
||||||
|
Amount int `yaml:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load parses a YAML file from disk.
|
||||||
|
func Load(path string) (File, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return File{}, fmt.Errorf("%w: %s", ErrConfigNotFound, path)
|
||||||
|
}
|
||||||
|
return File{}, fmt.Errorf("read config %s: %w", path, err)
|
||||||
|
}
|
||||||
|
var f File
|
||||||
|
if err := yaml.Unmarshal(data, &f); err != nil {
|
||||||
|
return File{}, fmt.Errorf("parse config %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply merges f onto dst. CLI-set fields (non-zero values in dst) win;
|
||||||
|
// YAML values fill in the rest.
|
||||||
|
func Apply(dst session.Config, f File) session.Config {
|
||||||
|
dst.Mode = pickString(dst.Mode, f.Mode)
|
||||||
|
dst.Link = pickString(dst.Link, f.Link)
|
||||||
|
dst.Transport = pickString(dst.Transport, f.Net.Transport)
|
||||||
|
dst.Auth = pickString(dst.Auth, f.Auth.Provider)
|
||||||
|
dst.Engine = pickString(dst.Engine, f.Engine.Name)
|
||||||
|
dst.URL = pickString(dst.URL, f.Engine.URL)
|
||||||
|
dst.Token = pickString(dst.Token, f.Engine.Token)
|
||||||
|
dst.RoomID = pickString(dst.RoomID, f.Room.ID)
|
||||||
|
dst.ClientID = pickString(dst.ClientID, f.Room.ClientID)
|
||||||
|
dst.KeyHex = pickString(dst.KeyHex, f.Crypto.Key)
|
||||||
|
dst.SOCKSHost = pickString(dst.SOCKSHost, f.SOCKS.Host)
|
||||||
|
dst.SOCKSPort = pickInt(dst.SOCKSPort, f.SOCKS.Port)
|
||||||
|
dst.SOCKSUser = pickString(dst.SOCKSUser, f.SOCKS.User)
|
||||||
|
dst.SOCKSPass = pickString(dst.SOCKSPass, f.SOCKS.Pass)
|
||||||
|
dst.DNSServer = pickString(dst.DNSServer, f.Net.DNS)
|
||||||
|
dst.SOCKSProxyAddr = pickString(dst.SOCKSProxyAddr, f.SOCKS.ProxyAddr)
|
||||||
|
dst.SOCKSProxyPort = pickInt(dst.SOCKSProxyPort, f.SOCKS.ProxyPort)
|
||||||
|
dst.VideoWidth = pickInt(dst.VideoWidth, f.Video.Width)
|
||||||
|
dst.VideoHeight = pickInt(dst.VideoHeight, f.Video.Height)
|
||||||
|
dst.VideoFPS = pickInt(dst.VideoFPS, f.Video.FPS)
|
||||||
|
dst.VideoBitrate = pickString(dst.VideoBitrate, f.Video.Bitrate)
|
||||||
|
dst.VideoHW = pickString(dst.VideoHW, f.Video.HW)
|
||||||
|
dst.VideoQRSize = pickInt(dst.VideoQRSize, f.Video.QRSize)
|
||||||
|
dst.VideoQRRecovery = pickString(dst.VideoQRRecovery, f.Video.QRRecovery)
|
||||||
|
dst.VideoCodec = pickString(dst.VideoCodec, f.Video.Codec)
|
||||||
|
dst.VideoTileModule = pickInt(dst.VideoTileModule, f.Video.TileModule)
|
||||||
|
dst.VideoTileRS = pickInt(dst.VideoTileRS, f.Video.TileRS)
|
||||||
|
dst.VP8FPS = pickInt(dst.VP8FPS, f.VP8.FPS)
|
||||||
|
dst.VP8BatchSize = pickInt(dst.VP8BatchSize, f.VP8.BatchSize)
|
||||||
|
dst.SEIFPS = pickInt(dst.SEIFPS, f.SEI.FPS)
|
||||||
|
dst.SEIBatchSize = pickInt(dst.SEIBatchSize, f.SEI.BatchSize)
|
||||||
|
dst.SEIFragmentSize = pickInt(dst.SEIFragmentSize, f.SEI.FragmentSize)
|
||||||
|
dst.SEIAckTimeoutMS = pickInt(dst.SEIAckTimeoutMS, f.SEI.AckTimeoutMS)
|
||||||
|
dst.Amount = pickInt(dst.Amount, f.Gen.Amount)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickString(cli, yamlVal string) string {
|
||||||
|
if cli != "" {
|
||||||
|
return cli
|
||||||
|
}
|
||||||
|
return yamlVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickInt(cli, yamlVal int) int {
|
||||||
|
if cli != 0 {
|
||||||
|
return cli
|
||||||
|
}
|
||||||
|
return yamlVal
|
||||||
|
}
|
||||||
93
internal/config/config_test.go
Normal file
93
internal/config/config_test.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/openlibrecommunity/olcrtc/internal/app/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadAndApply(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "olcrtc.yaml")
|
||||||
|
body := `
|
||||||
|
mode: srv
|
||||||
|
link: direct
|
||||||
|
auth:
|
||||||
|
provider: wbstream
|
||||||
|
room:
|
||||||
|
id: r1
|
||||||
|
client_id: c1
|
||||||
|
crypto:
|
||||||
|
key: deadbeef
|
||||||
|
net:
|
||||||
|
transport: datachannel
|
||||||
|
dns: 1.1.1.1:53
|
||||||
|
socks:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 1080
|
||||||
|
user: u
|
||||||
|
pass: p
|
||||||
|
vp8:
|
||||||
|
fps: 25
|
||||||
|
batch_size: 4
|
||||||
|
gen:
|
||||||
|
amount: 3
|
||||||
|
debug: true
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
if f.Mode != "srv" || f.Auth.Provider != "wbstream" || f.Room.ID != "r1" || f.Crypto.Key != "deadbeef" {
|
||||||
|
t.Fatalf("unexpected file: %+v", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := Apply(session.Config{}, f)
|
||||||
|
if got.Mode != "srv" || got.Link != "direct" || got.Auth != "wbstream" ||
|
||||||
|
got.RoomID != "r1" || got.ClientID != "c1" || got.KeyHex != "deadbeef" ||
|
||||||
|
got.Transport != "datachannel" || got.DNSServer != "1.1.1.1:53" ||
|
||||||
|
got.SOCKSHost != "127.0.0.1" || got.SOCKSPort != 1080 ||
|
||||||
|
got.SOCKSUser != "u" || got.SOCKSPass != "p" ||
|
||||||
|
got.VP8FPS != 25 || got.VP8BatchSize != 4 || got.Amount != 3 {
|
||||||
|
t.Fatalf("Apply produced wrong config: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCLIWins(t *testing.T) {
|
||||||
|
cli := session.Config{
|
||||||
|
Mode: "cnc",
|
||||||
|
KeyHex: "from-cli",
|
||||||
|
SOCKSPort: 9999,
|
||||||
|
}
|
||||||
|
f := File{
|
||||||
|
Mode: "srv",
|
||||||
|
Crypto: Crypto{Key: "from-yaml"},
|
||||||
|
SOCKS: SOCKS{Port: 1234, Host: "0.0.0.0"},
|
||||||
|
}
|
||||||
|
got := Apply(cli, f)
|
||||||
|
if got.Mode != "cnc" {
|
||||||
|
t.Errorf("Mode: got %q, want cnc (CLI wins)", got.Mode)
|
||||||
|
}
|
||||||
|
if got.KeyHex != "from-cli" {
|
||||||
|
t.Errorf("KeyHex: got %q, want from-cli (CLI wins)", got.KeyHex)
|
||||||
|
}
|
||||||
|
if got.SOCKSPort != 9999 {
|
||||||
|
t.Errorf("SOCKSPort: got %d, want 9999 (CLI wins)", got.SOCKSPort)
|
||||||
|
}
|
||||||
|
if got.SOCKSHost != "0.0.0.0" {
|
||||||
|
t.Errorf("SOCKSHost: got %q, want 0.0.0.0 (YAML fills empty CLI)", got.SOCKSHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadMissing(t *testing.T) {
|
||||||
|
_, err := Load(filepath.Join(t.TempDir(), "nope.yaml"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for missing file")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user