// 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() }