feat: implement M1 teleport protocol sender

Stream a synthetic test pattern and silent PCM audio over the OBS
Teleport protocol:
- protocol: wire format (Header/ImageHeader/WaveHeader), BT.709 full
  range colour matrix, JPEG encode via turbojpeg cgo, WAVE packet builder
- output: TCP sender with per-connection buffered channels and drop-on-overflow
- discovery: multicast announce via peerdiscovery
- cmd: teleportfling CLI with flags, test-pattern frame generator

Verified end-to-end: OBS discovers and renders the stream with correct
colours and motion.
This commit is contained in:
2026-09-18 18:36:25 +01:00
parent b8b2ba8da1
commit 10fa72b228
11 changed files with 1438 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
// Package output implements the TCP sender and receiver-connection manager
// for the Teleport protocol. It mirrors the design of obs-teleport's
// Sender: a per-connection buffered channel (capacity 1000), frame drop
// when the queue exceeds 800, and a warning at 100.
//
// Usage:
//
// s := output.New()
// s.Listen(":9756")
// s.Send(packet) // from any goroutine
// ...
// s.Close()
package output
import (
"log"
"net"
"sync"
)
const (
sendChanCap = 1000
dropAt = 800
warnAt = 100
)
// Sender manages a TCP listener and a set of connected receivers.
// Frames are distributed to every connected receiver; a receiver whose
// queue grows too large has its next frame silently dropped.
type Sender struct {
mu sync.Mutex
conns map[net.Conn]chan []byte
wg sync.WaitGroup
listener net.Listener
port int
}
// New creates an unconnected Sender.
func New() *Sender {
return &Sender{
conns: make(map[net.Conn]chan []byte),
}
}
// Listen binds a TCP listener on addr (e.g. ":9756") and starts accepting
// connections. It returns the resolved port number on success. The caller
// must eventually call Close.
func (s *Sender) Listen(addr string) (int, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return 0, err
}
s.listener = l
// Extract the port so the caller can advertise it via UDP announce.
_, p, err := net.SplitHostPort(l.Addr().String())
if err != nil {
if cerr := l.Close(); cerr != nil {
log.Printf("output: close failed: %v", cerr)
}
return 0, err
}
var port int
for _, ch := range p {
port = port*10 + int(ch-'0')
}
s.port = port
s.wg.Add(1)
go s.acceptLoop()
return port, nil
}
// Port returns the TCP port that was assigned at Listen time.
func (s *Sender) Port() int { return s.port }
// NumConns returns the number of connected receivers.
func (s *Sender) NumConns() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.conns)
}
// Send broadcasts a serialised packet to all connected receivers. If a
// receiver's buffered channel is full (> dropAt) the frame is silently
// dropped; a warning is logged at warnAt.
func (s *Sender) Send(b []byte) {
s.mu.Lock()
defer s.mu.Unlock()
for c, ch := range s.conns {
switch {
case len(ch) > dropAt:
log.Printf("output: drop [%s] (queue %d)", c.RemoteAddr(), len(ch))
case len(ch) > warnAt:
log.Printf("output: high [%s] (queue %d)", c.RemoteAddr(), len(ch))
default:
}
if len(ch) > dropAt {
continue
}
// Non-blocking send: if the channel is full after the drop
// window, drop the frame.
select {
case ch <- b:
default:
log.Printf("output: drop [%s] (queue full)", c.RemoteAddr())
}
}
}
// Close shuts down the listener and waits for all writer goroutines to
// drain. After Close returns the Sender must not be reused.
func (s *Sender) Close() {
if s.listener != nil {
// Best-effort close; the accept loop will observe the listener error.
_ = s.listener.Close()
}
s.mu.Lock()
for _, ch := range s.conns {
close(ch)
}
s.conns = nil
s.mu.Unlock()
s.wg.Wait()
}
// acceptLoop runs in its own goroutine and adds incoming connections.
func (s *Sender) acceptLoop() {
defer s.wg.Done()
for {
c, err := s.listener.Accept()
if err != nil {
return
}
s.SenderAdd(c)
}
}
// SenderAdd registers a new receiver connection and spawns its writer.
func (s *Sender) SenderAdd(c net.Conn) {
s.mu.Lock()
defer s.mu.Unlock()
log.Printf("output: connect %s", c.RemoteAddr())
ch := make(chan []byte, sendChanCap)
s.conns[c] = ch
s.wg.Add(1)
go func() {
defer s.wg.Done()
defer func() {
// Best-effort close; writer loop also closes on write error.
_ = c.Close()
}()
for b := range ch {
if _, err := c.Write(b); err != nil {
log.Printf("output: disconnect %s", c.RemoteAddr())
s.removeConn(c)
return
}
}
}()
}
func (s *Sender) removeConn(c net.Conn) {
s.mu.Lock()
delete(s.conns, c)
s.mu.Unlock()
}
+61
View File
@@ -0,0 +1,61 @@
package output
import (
"io"
"net"
"testing"
"time"
)
// TestSendReceive does an end-to-end round trip: a TCP client connects to
// the Sender, Send broadcasts a frame, and the client must read it back.
func TestSendReceive(t *testing.T) {
s := New()
port, err := s.Listen("127.0.0.1:0")
if err != nil {
t.Fatalf("Listen: %v", err)
}
conn, err := net.Dial("tcp", "127.0.0.1:"+itoa(port))
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer func() { _ = conn.Close() }()
defer func() { _ = conn.(*net.TCPConn).SetLinger(0) }()
// Give the accept loop a moment to register the connection.
deadline := time.Now().Add(2 * time.Second)
for s.NumConns() == 0 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
if s.NumConns() != 1 {
t.Fatalf("NumConns = %d, want 1", s.NumConns())
}
payload := []byte("frame-one")
s.Send(payload)
got := make([]byte, len(payload))
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatalf("read: %v", err)
}
if string(got) != "frame-one" {
t.Errorf("round trip mismatch: got %q", got)
}
s.Close()
}
func itoa(v int) string {
if v == 0 {
return "0"
}
var buf [20]byte
i := len(buf)
for v > 0 {
i--
buf[i] = byte('0' + v%10)
v /= 10
}
return string(buf[i:])
}