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)
|
||||
|
||||
Reference in New Issue
Block a user