- sender tracks payload bytes; engine measures stream bitrate over a sliding window and exposes it in Status - GUI status label and tray tooltip show the live bitrate (e.g. 36 Mbps) - add a Preset dropdown (Low/Medium/High/Ultra) that sets quality+fps together and applies live via SetConfig Verified: 36 Mbps shown for Medium (quality 70 @ 30fps) matches the wire measurement.
201 lines
4.4 KiB
Go
201 lines
4.4 KiB
Go
// 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"
|
|
"sync/atomic"
|
|
)
|
|
|
|
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
|
|
|
|
dropped atomic.Int64
|
|
bytes atomic.Int64
|
|
}
|
|
|
|
// 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. Dropped frames are counted and
|
|
// exposed via Dropped.
|
|
func (s *Sender) Send(b []byte) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
// Count the packet once as produced bandwidth (independent of how many
|
|
// receivers are attached).
|
|
s.bytes.Add(int64(len(b)))
|
|
|
|
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 {
|
|
s.dropped.Add(1)
|
|
continue
|
|
}
|
|
|
|
// Non-blocking send: if the channel is full after the drop
|
|
// window, drop the frame.
|
|
select {
|
|
case ch <- b:
|
|
default:
|
|
s.dropped.Add(1)
|
|
log.Printf("output: drop [%s] (queue full)", c.RemoteAddr())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dropped returns the total number of frames dropped due to backpressure.
|
|
func (s *Sender) Dropped() int64 {
|
|
return s.dropped.Load()
|
|
}
|
|
|
|
// BytesSent returns the total number of payload bytes handed to Send.
|
|
func (s *Sender) BytesSent() int64 {
|
|
return s.bytes.Load()
|
|
}
|
|
|
|
// 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()
|
|
}
|