Add config.LoadFrom/SaveTo for arbitrary paths and thread a --config flag through both entry points: - CLI: --config loads a file first; explicit flags override file values - GUI: --config selects the settings file used for load and save - tests for LoadFrom/SaveTo and default fallback
124 lines
3.8 KiB
Go
124 lines
3.8 KiB
Go
// Command teleportfling is a standalone sender for the Teleport protocol.
|
|
//
|
|
// It captures a Wayland screen (PipeWire via xdg-desktop-portal) plus the
|
|
// system's default audio output and streams them over TCP as the Teleport
|
|
// protocol, announcing itself on the LAN multicast group so an OBS instance
|
|
// with the obs-teleport plugin can discover and decode the stream.
|
|
//
|
|
// This is the headless/CLI entry point; the engine lives in
|
|
// internal/flinger. A desktop GUI with settings and a system tray is in
|
|
// cmd/teleportfling-gui.
|
|
//
|
|
// Usage:
|
|
//
|
|
// teleportfling [--name NAME] [--port PORT] [--quality 1..100]
|
|
// [--fps N] [--source screen|pattern] [--audio]
|
|
// [--stream-index N] [--duration SECONDS]
|
|
// [--config PATH]
|
|
//
|
|
// --source pattern selects the M1 synthetic test pattern (colour bars with a
|
|
// moving box) instead of real screen capture, which is useful for testing
|
|
// without granting screen-share permission.
|
|
//
|
|
// --config loads a saved config file first; any flag given explicitly on the
|
|
// command line overrides the file value. When running under a service manager
|
|
// (e.g. a systemd user unit) use --config to point at the daemon's profile.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"teleportfling/internal/config"
|
|
"teleportfling/internal/flinger"
|
|
)
|
|
|
|
func main() {
|
|
var (
|
|
name = flag.String("name", "", "announce name (default: hostname)")
|
|
port = flag.Int("port", 9756, "TCP listening port")
|
|
quality = flag.Int("quality", 80, "JPEG quality 1..100")
|
|
fps = flag.Int("fps", 30, "video frames per second")
|
|
source = flag.String("source", "screen", "capture source: screen or pattern")
|
|
withAudio = flag.Bool("audio", true, "capture and stream system audio")
|
|
noAnnounce = flag.Bool("no-announce", false, "do not announce on the LAN (receiver must connect by IP)")
|
|
streamIndex = flag.Int("stream-index", 0, "monitor index to capture (screen source)")
|
|
duration = flag.Duration("duration", 0, "stream duration (0 = run until interrupted)")
|
|
configPath = flag.String("config", "", "config file path (default: ~/.config/teleportfling/config.json)")
|
|
)
|
|
flag.Parse()
|
|
|
|
// Base config: loaded from file (or defaults when absent), then overridden
|
|
// by any flag the user explicitly set.
|
|
cfg := loadCLIConfig(*configPath)
|
|
flag.Visit(func(f *flag.Flag) {
|
|
switch f.Name {
|
|
case "name":
|
|
cfg.Name = *name
|
|
case "port":
|
|
cfg.Port = *port
|
|
case "quality":
|
|
cfg.Quality = *quality
|
|
case "fps":
|
|
cfg.FPS = *fps
|
|
case "source":
|
|
cfg.Source = *source
|
|
case "audio":
|
|
cfg.Audio = *withAudio
|
|
case "no-announce":
|
|
cfg.Announce = !*noAnnounce
|
|
case "stream-index":
|
|
cfg.StreamIndex = *streamIndex
|
|
}
|
|
})
|
|
|
|
eng, err := flinger.New(cfg)
|
|
if err != nil {
|
|
log.Fatalf("flinger: %v", err)
|
|
}
|
|
|
|
eng.Start()
|
|
log.Printf("teleportfling: streaming from source %q", cfg.Source)
|
|
|
|
// Interrupt / SIGTERM handling.
|
|
sigc := make(chan os.Signal, 1)
|
|
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
select {
|
|
case <-sigc:
|
|
log.Printf("teleportfling: stopping…")
|
|
case <-func() <-chan struct{} {
|
|
if *duration > 0 {
|
|
ch := make(chan struct{})
|
|
time.AfterFunc(*duration, func() { close(ch) })
|
|
return ch
|
|
}
|
|
return nil
|
|
}():
|
|
log.Printf("teleportfling: duration reached")
|
|
}
|
|
|
|
eng.Stop()
|
|
log.Printf("teleportfling: stopped")
|
|
}
|
|
|
|
// loadCLIConfig returns the base flinger config. With a --config path it reads
|
|
// that file; otherwise it reads the default user config. Missing files fall
|
|
// back to defaults.
|
|
func loadCLIConfig(path string) flinger.Config {
|
|
p := path
|
|
if p == "" {
|
|
p = config.Path()
|
|
}
|
|
c, err := config.LoadFrom(p)
|
|
if err != nil {
|
|
log.Printf("teleportfling: config %s: %v (using defaults)", p, err)
|
|
return flinger.DefaultConfig()
|
|
}
|
|
return c.ToFlinger()
|
|
}
|