- config: validate port/quality/fps/source/stream-index ranges on load and before engine start (flinger.Config.Validate) - discovery: add Announce option to disable multicast (GUI checkbox, CLI --no-announce); absent JSON key keeps the default true - backpressure: count dropped frames in the TCP sender, expose via engine Status and a live counter in the GUI status label - docs: record M3/M4 decisions and X11 coverage via the portal backend
10 KiB
10 KiB
TeleportFling - Project Overview
Summary
TeleportFling is a standalone Linux application that captures a single screen along with
audio from the host machine and transmits the combined stream over a LAN using the
Teleport protocol. The teleport stream is received
by another machine running OBS Studio with the obs-teleport
plugin installed, where it appears as a regular Teleport source.
The project is an OBS/NDI-like replacement for a minimal multi-machine streaming setup: one machine (the "flinger") produces a screen + audio feed, a second machine aggregates and streams it. No NDI compatibility of any form.
Goals
- Capture a single monitor (screen) + configurable audio source on Linux.
- Push the stream over the LAN using the Teleport protocol so an OBS +
obs-teleportreceiver can consume it with zero plugin-side configuration on the source machine. - Ship a standalone, installable application, not an OBS plugin:
- Full configuration GUI (device, audio source, quality, discovery settings).
- System tray icon for quick start/stop and status.
- Works headless-ish: capture/transmit logic is decoupled from the GUI so it can later run as a daemon or be scripted.
Non-Goals
- NDI or DistroAV compatibility.
- Software transcoding beyond the JPEG frame encoding the protocol requires.
- Synchronous multi-screen capture initially (single screen only, per the name).
- Windows/macOS support initially (Linux only).
Target Environment
| Concern | Decision |
|---|---|
| Language | Go (same language as obs-teleport) |
| Display server | Wayland primary; X11 support if it falls out cheaply |
| Audio | Configurable: system/desktop audio (default) or mic/input device |
| Screen capture | Wayland: PipeWire / xdg-desktop-portal; X11: X11 extension or PipeWire fallback |
| GUI | Fyne (config window + system tray) |
| Discovery | UDP multicast peer discovery (identical to obs-teleport) |
| Target quality | Initial release: 1920x1080 @ 30 fps, balanced JPEG quality |
| Protocol target | Latest obs-teleport release (protocol contract below) |
| Remote git | Self-hosted Gitea at http://homeserver:3050/ |
Teleport Protocol Contract
Reference implementation: https://github.com/fzwoch/obs-teleport (GPL-2.0).
- Peer discovery - UDP multicast broadcast of a JSON
AnnouncePayload:{"Name", "Port", "AudioAndVideo", "Version", "Address"}. The OBS receiver uses these announcements to populate its source list.obs-teleportusesgithub.com/schollz/peerdiscovery. - Transport - TCP. The sender binds a listener on a configurable port and accepts connections from receivers (multiple concurrent receivers supported).
- Packet header (all little-endian):
Header:Type [4]byte,Timestamp uint64,Size int32.ImageHeader(video only):ColorMatrix [16]float32,ColorRangeMin [3]float32,ColorRangeMax [3]float32(from OBS video format color parameters).WaveHeader(audio only):Format int32,SampleRate int32,Speakers int32,Frames int32.
- Video frames -
Type = "JPEG". Frames are compressed to JPEG (turbojpeg) with configurable quality. Payload = header + image header + JPEG bytes. - Audio -
Type = "WAVE". Raw interleaved PCM. Payload = header + wave header + PCM samples (16-bit stereo @ 48 kHz typical). - Ordering - Video frames are queued, encoded in order, and sent in the same order; dropped frames are counted as lagged frames. Audio is sent immediately as it arrives.
- Compatibility contract - We must remain byte-compatible with the latest
obs-teleportrelease so that stock OBS Teleport receivers can discover and decode our stream without modification.
Architecture (proposed)
┌────────────────────────────┐ ┌───────────────────────────────┐
│ Fling app │ │ OBS receiver │
│ ┌──────────┐ ┌─────────┐ │ │ │
│ │ Screen │ │ Audio │ │ │ ┌─────────────────────────┐ │
│ │ capture │ │ capture │ │ │ │ obs-teleport source │ │
│ └────┬─────┘ └────┬────┘ │ │ └───────────┬─────────────┘ │
│ │ │ │ │ │ │
│ ▼ ▼ │ │ ▼ │
│ ┌────────────────────────┐ │ │ ┌─────────────────────────┐ │
│ │ Flinger core │ │ │ │ (decode + render) │ │
│ │ · JPEG encode (video) │ ├─────┼─▶│ OBS Studio pipeline │ │
│ │ · interleave (audio) │ │ │ └─────────────────────────┘ │
│ │ · packetize │ │ │ │
│ │ · TCP sender │ │ │ │
│ │ · UDP announcer │ │ │ │
│ └───────────┬────────────┘ │ │ │
└──────────────┼──────────────┘ └───────────────────────────────┘
│ LAN (TCP + UDP multicast)
Components:
- Core library (
internal/flinger) - protocol + capture agnostic:protocol: packet types, JPEG encode, WAVE packetize, header (de)serialization.output: TCP listener, connection management (reuseobs-teleport's per-connection buffered channel approach with drop-when-overflow), framing/ordering queue.discovery: UDP multicast announcer (AnnouncePayload).capture: interfaceCaptureSource+ implementations for screen and audio.
- GUI (
cmd/teleportflingorinternal/gui) - Fyne:- Settings window: monitor picker, audio source picker, port, JPEG quality, resolution/fps targets, discovery on/off, enable/disable.
- System tray: start/stop toggle, status indicator, open settings, quit.
- Config persistence (JSON config file, e.g.
~/.config/teleportfling/config.json).
- Capture backend:
- Wayland: PipeWire via
pipewireGo bindings orxdg-desktop-portalscreen capture. - Audio: PipeWire/PulseAudio monitor source (system audio) or input device; configurable.
- X11 (optional): X11 API screen grab or PipeWire fallback where available.
- Wayland: PipeWire via
Milestones
- M1 - Protocol proof of life: static JPEG frames + synthesized audio sent over TCP, multicast announcements read successfully by OBS Teleport receiver. CLI only.
- M2 - Real capture: PipeWire screen capture + system audio capture feeding the core; confirm 1080p30 on Wayland.
- M3 - GUI + tray: Fyne settings window and system tray start/stop.
- M4 - Hardening: config persistence, frame dropping/backpressure, discovery options, X11 fallback if feasible, packaging (Nix package / AppImage / release binaries).
Development Environment
- Nix flake (
flake.nix) withdevShellworkspace. direnvmanages activating the Nix environment.- Build:
go build ./...; Lint:golangci-lint; Tests:go test ./.... - All development follows the rules in
AGENTS.md.
Open Questions
- Bitrate/bandwidth targets beyond the balanced 1080p30 default.
- Whether to support scaling/downsampling of the captured monitor in M1/M2 or only the monitor's native resolution.
- PipeWire bindings strategy (pure-Go bindings vs. cgo wrappers vs.
xdg-desktop-portal).
Decisions Made (2026-09-18)
- Name: project (and binary) is
teleportfling. - JPEG encoding: match
obs-teleportexactly - use turbojpeg via cgo (libturbojpeg). Benchmark against a pure-Go encoder during M1 anyway; use turbojpeg unless it is a blocker. - System tray: Fyne's native tray support is limited on Linux; evaluate
github.com/nicedoc/systray(or similar) when we reach M3. Do not block M1/M2 on it. - PipeWire bindings: unresolved - evaluate pure-Go bindings vs. cgo to libpipewire during M1. This drives capture backend build requirements.
- End-to-end testing: OBS +
obs-teleportare available locally on the dev machine for now (loopback/LAN testing). A remote receiver machine can be added later for further testing. - License: GPL-2.0, matching the
obs-teleportproject whose protocol we implement.
Decisions Made (2026-09-18, M3/M4)
- Screen capture backend:
go2tv.app/screencast(MIT, GPL-2.0-compatible) viaxdg-desktop-portal+ PipeWire. Chosen over hand-rolled cgo bindings. The portal path is compositor-agnostic: Wayland (hyprland/gnome portals) and X11 (xdg-desktop-portal-gtk) both work, so a separate native X11/XShm fallback is not required for the supported path and was not implemented (project.md M4 "if feasible"). - GUI toolkit: Fyne v2 — provides both the settings window and a system tray (StatusNotifierItem over DBus), so no separate systray dependency was needed.
- Engine decoupling: streaming core lives in
internal/flinger(GUI-free) so the CLI, GUI and any future daemon share one implementation.
M4 Hardening Status
- Config persistence + validation: done (internal/config, flinger.Validate).
- Backpressure/queue stats: done (sender tracks dropped frames; exposed in engine Status and the GUI status label).
- Discovery on/off: done (flinger.Config.Announce, GUI checkbox).
- X11 fallback: not implemented — the portal backend already covers X11 sessions.
- Packaging (Nix package / AppImage): deferred (see AGENTS.md / roadmap).