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:
2026-09-19 17:37:47 +01:00
parent f25b498fc1
commit 95f47abadb
3 changed files with 128 additions and 6 deletions
+45 -4
View File
@@ -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)