feat: harden engine config, discovery and backpressure stats

- 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
This commit is contained in:
2026-09-18 20:44:39 +01:00
parent cb27a0d63d
commit 2b3fd54934
8 changed files with 195 additions and 21 deletions
+14
View File
@@ -23,11 +23,15 @@ type Config struct {
Source string `json:"source"`
Audio bool `json:"audio"`
StreamIndex int `json:"stream_index"`
// Announce is a *bool so an absent JSON key (older config files) keeps
// the default instead of silently disabling announcements.
Announce *bool `json:"announce"`
}
// Default returns the default configuration.
func Default() Config {
c := flinger.DefaultConfig()
announce := c.Announce
return Config{
Name: c.Name,
Port: c.Port,
@@ -36,11 +40,16 @@ func Default() Config {
Source: c.Source,
Audio: c.Audio,
StreamIndex: c.StreamIndex,
Announce: &announce,
}
}
// ToFlinger converts a persisted config to the engine config.
func (c Config) ToFlinger() flinger.Config {
announce := true
if c.Announce != nil {
announce = *c.Announce
}
return flinger.Config{
Name: c.Name,
Port: c.Port,
@@ -49,6 +58,7 @@ func (c Config) ToFlinger() flinger.Config {
Source: c.Source,
Audio: c.Audio,
StreamIndex: c.StreamIndex,
Announce: announce,
}
}
@@ -96,6 +106,10 @@ func Load() (Config, error) {
if c.Source == "" {
c.Source = d.Source
}
if c.Announce == nil {
announce := true
c.Announce = &announce
}
return c, nil
}
+18 -1
View File
@@ -29,6 +29,7 @@ func TestSaveLoadRoundTrip(t *testing.T) {
pathVar = filepath.Join(t.TempDir(), "config.json")
defer func() { pathVar = old }()
announce := false
want := Config{
Name: "Studio",
Port: 9898,
@@ -37,6 +38,7 @@ func TestSaveLoadRoundTrip(t *testing.T) {
Source: "pattern",
Audio: false,
StreamIndex: 1,
Announce: &announce,
}
if err := Save(want); err != nil {
t.Fatalf("Save: %v", err)
@@ -46,7 +48,10 @@ func TestSaveLoadRoundTrip(t *testing.T) {
if err != nil {
t.Fatalf("Load: %v", err)
}
if got != want {
if got.Name != want.Name || got.Port != want.Port || got.Quality != want.Quality ||
got.FPS != want.FPS || got.Source != want.Source || got.Audio != want.Audio ||
got.StreamIndex != want.StreamIndex || got.Announce == nil || want.Announce == nil ||
*got.Announce != *want.Announce {
t.Errorf("round trip mismatch:\n got %+v\nwant %+v", got, want)
}
}
@@ -74,4 +79,16 @@ func TestLoadFillsZeroValues(t *testing.T) {
if c.Quality != 80 {
t.Errorf("quality = %d, want filled default 80", c.Quality)
}
// Absent "announce" key must default to true (not false).
if c.Announce == nil || !*c.Announce {
t.Errorf("announce = %v, want default true", c.Announce)
}
}
// TestToFlingerAnnounceDefault checks the announce default survives conversion.
func TestToFlingerAnnounceDefault(t *testing.T) {
c := Config{}
if !c.ToFlinger().Announce {
t.Error("ToFlinger announce default should be true")
}
}