Files
olcrtc/cmd/olcrtc/main.go
2026-04-27 06:11:25 +03:00

217 lines
5.9 KiB
Go

// Package main provides the olcrtc CLI entrypoint.
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/openlibrecommunity/olcrtc/internal/app/session"
"github.com/openlibrecommunity/olcrtc/internal/logger"
"github.com/openlibrecommunity/olcrtc/internal/names"
)
type config struct {
mode string
link string
transport string
carrier string
roomID string
provider string
socksPort int
socksHost 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
videoCodec string
videoBModule int
videoBColors int
vp8FPS int
vp8BatchSize int
}
func main() {
if err := run(); err != nil {
logger.Error(err)
os.Exit(1)
}
}
func run() error {
session.RegisterDefaults()
cfg := parseFlags()
configureLogging(cfg.debug)
if err := session.Validate(toSessionConfig(cfg)); err != nil {
return err
}
if cfg.dataDir == "" {
return fmt.Errorf("data directory required (use -data data)")
}
dataDir, err := resolveDataDir(cfg.dataDir)
if err != nil {
return err
}
if err := loadNames(dataDir); err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
errCh := make(chan error, 1)
go func() {
errCh <- session.Run(ctx, toSessionConfig(cfg))
}()
select {
case <-sigCh:
logger.Info("Shutting down gracefully...")
cancel()
return waitForShutdown(errCh)
case err := <-errCh:
return err
}
}
func parseFlags() config {
cfg := config{}
flag.StringVar(&cfg.mode, "mode", "", "Mode: srv or cnc")
flag.StringVar(&cfg.link, "link", "", "Link: direct (p2p connection type)")
flag.StringVar(&cfg.transport, "transport", "", "Transport: datachannel, videochannel, seichannel")
flag.StringVar(&cfg.carrier, "carrier", "", "Carrier: telemost, jazz, wbstream")
flag.StringVar(&cfg.roomID, "id", "", "Room ID")
flag.StringVar(&cfg.provider, "provider", "", "Deprecated alias for -carrier")
flag.IntVar(&cfg.socksPort, "socks-port", 0, "SOCKS5 port (client only)")
flag.StringVar(&cfg.socksHost, "socks-host", "", "SOCKS5 listen host (client only)")
flag.StringVar(&cfg.keyHex, "key", "", "Shared encryption key (hex)")
flag.BoolVar(&cfg.debug, "debug", false, "Enable verbose logging")
flag.StringVar(&cfg.dataDir, "data", "", "Path to data directory")
flag.StringVar(&cfg.dnsServer, "dns", "", "DNS server (e.g. 1.1.1.1:53)")
flag.StringVar(&cfg.socksProxyAddr, "socks-proxy", "", "SOCKS5 proxy address (server only)")
flag.IntVar(&cfg.socksProxyPort, "socks-proxy-port", 0, "SOCKS5 proxy port (server only)")
flag.IntVar(&cfg.videoWidth, "video-w", 0, "Video logical width (videochannel only)")
flag.IntVar(&cfg.videoHeight, "video-h", 0, "Video logical height (videochannel only)")
flag.IntVar(&cfg.videoFPS, "video-fps", 0, "Video frames per second (videochannel only)")
flag.StringVar(&cfg.videoBitrate, "video-bitrate", "", "Video bitrate (videochannel only)")
flag.StringVar(&cfg.videoHW, "video-hw", "", "Hardware acceleration (none, nvenc)")
flag.IntVar(&cfg.videoQRSize, "video-qr-size", 0, "Video QR code fragment size (videochannel only)")
flag.StringVar(&cfg.videoCodec, "video-codec", "qrcode", "Visual codec: qrcode (slow, stable) or b (fast, unstable)")
flag.IntVar(&cfg.videoBModule, "video-b-module", 4, "B codec pixels per module (default 4)")
flag.IntVar(&cfg.videoBColors, "video-b-colors", 8, "B codec colors (4, 8, 16, 32, 64, 128, 256)")
flag.IntVar(&cfg.vp8FPS, "vp8-fps", 0, "VP8 frames per second (vp8channel only, default 25)")
flag.IntVar(&cfg.vp8BatchSize, "vp8-batch", 0, "VP8 frames per tick (vp8channel only, default 1)")
flag.Parse()
return cfg
}
func configureLogging(debug bool) {
if debug {
logger.SetVerbose(true)
}
}
func resolveDataDir(dataDir string) (string, error) {
if filepath.IsAbs(dataDir) {
return dataDir, nil
}
exePath, err := os.Executable()
if err != nil {
return "", fmt.Errorf("resolve executable path: %w", err)
}
return filepath.Join(filepath.Dir(exePath), dataDir), nil
}
func loadNames(dataDir string) error {
namesPath := filepath.Join(dataDir, "names")
surnamesPath := filepath.Join(dataDir, "surnames")
if err := names.LoadNameFiles(namesPath, surnamesPath); err != nil {
return fmt.Errorf("load embedded names override: %w", err)
}
return nil
}
func toSessionConfig(cfg config) session.Config {
return session.Config{
Mode: cfg.mode,
Link: cfg.link,
Transport: cfg.transport,
Carrier: firstNonEmpty(cfg.carrier, cfg.provider),
RoomID: cfg.roomID,
KeyHex: cfg.keyHex,
SOCKSHost: cfg.socksHost,
SOCKSPort: cfg.socksPort,
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,
VideoCodec: cfg.videoCodec,
VideoBModule: cfg.videoBModule,
VideoBColors: cfg.videoBColors,
VP8FPS: cfg.vp8FPS,
VP8BatchSize: cfg.vp8BatchSize,
}
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
func waitForShutdown(errCh <-chan error) error {
done := make(chan error, 1)
go func() {
if err := <-errCh; err != nil {
done <- err
} else {
done <- nil
}
}()
select {
case err := <-done:
if err == nil {
logger.Info("Shutdown complete")
}
return err
case <-time.After(5 * time.Second):
logger.Warn("Shutdown timeout, forcing exit")
return nil
}
}