feat: add bitrate measurement and quality presets

- 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.
This commit is contained in:
2026-09-19 17:53:49 +01:00
parent 95f47abadb
commit 583274d9e6
4 changed files with 172 additions and 3 deletions
+57
View File
@@ -129,6 +129,63 @@ func TestValidate(t *testing.T) {
}
}
// TestBitrateMeasurement verifies the engine reports a non-zero bitrate once
// it has been streaming for a bit.
func TestBitrateMeasurement(t *testing.T) {
cfg := DefaultConfig()
cfg.Source = "pattern"
cfg.Port = 19759
eng, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
eng.Start()
defer eng.Stop()
// Connect a receiver so packets actually flow, and wait for the bitrate
// window to produce a measurement.
conn, err := net.Dial("tcp", "127.0.0.1:"+itoa(eng.sender.Port()))
if err != nil {
t.Fatalf("dial: %v", err)
}
defer func() { _ = conn.Close() }()
go func() {
buf := make([]byte, 64*1024)
for {
if _, err := conn.Read(buf); err != nil {
return
}
}
}()
deadline := time.Now().Add(4 * time.Second)
for time.Now().Before(deadline) {
if eng.Status().Bitrate > 0 {
return
}
time.Sleep(200 * time.Millisecond)
}
t.Error("bitrate stayed 0 after 4s of streaming")
}
// TestFormatBitrate checks the human-readable bitrate formatting.
func TestFormatBitrate(t *testing.T) {
cases := []struct {
bps int64
want string
}{
{500, "500 bps"},
{5_000, "5 kbps"},
{5_000_000, "5.0 Mbps"},
}
for _, c := range cases {
if got := formatBitrate(c.bps); got != c.want {
t.Errorf("formatBitrate(%d) = %q, want %q", c.bps, got, c.want)
}
}
}
func itoa(v int) string {
if v == 0 {
return "0"