feat: live settings changes mid-stream
Add flinger.SetConfig to update FPS/quality/name/audio/announce without restarting the stream. Fields that require a restart (source, port, stream-index) are preserved. The video loop reads the live config each frame so changes apply immediately, and the GUI Quality/Frame-rate dropdowns push changes to a running engine. Verified live: fps 12->33 and JPEG size 210KB->1.3MB on quality change.
This commit is contained in:
@@ -99,7 +99,8 @@ type Status struct {
|
||||
|
||||
// Engine owns the capture, encode and send pipeline.
|
||||
type Engine struct {
|
||||
cfg Config
|
||||
cfgMu sync.RWMutex
|
||||
cfg Config
|
||||
|
||||
sender *output.Sender
|
||||
announcer *discovery.Announcer
|
||||
@@ -122,6 +123,35 @@ func (e *Engine) setErr(err error) {
|
||||
e.errMu.Unlock()
|
||||
}
|
||||
|
||||
// SetConfig updates engine settings live (FPS, quality, name, etc.) without
|
||||
// restarting the stream. It validates the new config first; on error the
|
||||
// engine keeps its current settings.
|
||||
//
|
||||
// Not every field is live-applicable mid-stream: source, port and
|
||||
// stream-index still require a restart (they are ignored if changed).
|
||||
func (e *Engine) SetConfig(cfg Config) error {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
e.cfgMu.Lock()
|
||||
defer e.cfgMu.Unlock()
|
||||
|
||||
// Fields that cannot change live keep their current values.
|
||||
cfg.Source = e.cfg.Source
|
||||
cfg.Port = e.cfg.Port
|
||||
cfg.StreamIndex = e.cfg.StreamIndex
|
||||
|
||||
e.cfg = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCfg returns a snapshot of the current config.
|
||||
func (e *Engine) getCfg() Config {
|
||||
e.cfgMu.RLock()
|
||||
defer e.cfgMu.RUnlock()
|
||||
return e.cfg
|
||||
}
|
||||
|
||||
// New creates an engine from cfg. Capture is opened eagerly so that
|
||||
// misconfiguration (e.g. no screen-share permission) surfaces before Start.
|
||||
func New(cfg Config) (*Engine, error) {
|
||||
@@ -274,9 +304,11 @@ func (e *Engine) audioLoop(src io.ReadCloser) {
|
||||
}
|
||||
}
|
||||
|
||||
// videoLoop pulls frames and sends them at the configured fps.
|
||||
// videoLoop pulls frames and sends them at the configured fps. The frame rate
|
||||
// and JPEG quality are read from the live config so SetConfig takes effect
|
||||
// without restarting.
|
||||
func (e *Engine) videoLoop() {
|
||||
frameInterval := time.Second / time.Duration(e.cfg.FPS)
|
||||
frameInterval := time.Second / time.Duration(e.getCfg().FPS)
|
||||
next := e.start
|
||||
|
||||
for {
|
||||
@@ -294,6 +326,15 @@ func (e *Engine) videoLoop() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Re-read the live config each frame so FPS/quality changes apply
|
||||
// immediately. When the interval changes, resync `next` to now.
|
||||
cfg := e.getCfg()
|
||||
interval := time.Second / time.Duration(cfg.FPS)
|
||||
if interval != frameInterval {
|
||||
frameInterval = interval
|
||||
next = time.Now()
|
||||
}
|
||||
|
||||
// Drop frames when running ahead of the target fps to keep
|
||||
// timestamps monotonic (e.g. a 60 Hz monitor captured at 30 fps).
|
||||
now := time.Now()
|
||||
@@ -303,7 +344,7 @@ func (e *Engine) videoLoop() {
|
||||
next = now.Add(frameInterval)
|
||||
|
||||
ts := uint64(now.Sub(e.start))
|
||||
buf, err := e.encoder.EncodeBGRA(frame.Pix, frame.Width, frame.Height, e.cfg.Quality)
|
||||
buf, err := e.encoder.EncodeBGRA(frame.Pix, frame.Width, frame.Height, cfg.Quality)
|
||||
if err != nil {
|
||||
e.setErr(err)
|
||||
log.Printf("flinger: jpeg: %v", err)
|
||||
|
||||
@@ -142,3 +142,50 @@ func itoa(v int) string {
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
// TestSetConfig verifies live config updates apply FPS/quality and reject
|
||||
// invalid values, while preserving fields that can't change live.
|
||||
func TestSetConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Source = "pattern"
|
||||
cfg.Port = 19758
|
||||
|
||||
eng, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
// Update FPS/quality live.
|
||||
newCfg := DefaultConfig()
|
||||
newCfg.Source = "screen" // must be ignored (requires restart)
|
||||
newCfg.Port = 9999 // must be ignored
|
||||
newCfg.Quality = 95
|
||||
newCfg.FPS = 60
|
||||
if err := eng.SetConfig(newCfg); err != nil {
|
||||
t.Fatalf("SetConfig: %v", err)
|
||||
}
|
||||
|
||||
got := eng.getCfg()
|
||||
if got.Quality != 95 {
|
||||
t.Errorf("quality = %d, want 95", got.Quality)
|
||||
}
|
||||
if got.FPS != 60 {
|
||||
t.Errorf("fps = %d, want 60", got.FPS)
|
||||
}
|
||||
// Source/port preserved.
|
||||
if got.Source != "pattern" {
|
||||
t.Errorf("source = %q, want pattern (unchanged live)", got.Source)
|
||||
}
|
||||
if got.Port != 19758 {
|
||||
t.Errorf("port = %d, want 19758 (unchanged live)", got.Port)
|
||||
}
|
||||
|
||||
// Invalid config is rejected and current settings kept.
|
||||
if err := eng.SetConfig(Config{Quality: 200}); err == nil {
|
||||
t.Error("expected error for invalid quality")
|
||||
}
|
||||
got = eng.getCfg()
|
||||
if got.Quality != 95 {
|
||||
t.Errorf("quality changed after rejected update: %d", got.Quality)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user