Bound live-stream latency drift with periodic live-edge resync
Over hours the wall drifted ~15s behind real-time: the Pi decodes a hair slower than real-time, and since RTSP-over-TCP delivers every byte, that small deficit buffers up instead of being dropped — and a live stream can't be seeked back to the live edge. Add player.resync_seconds (0 = off): the daemon reconnects each stream to the live edge on that interval via mpv loadfile-replace over IPC, cycling one tile at a time so only a single tile ever blips. Reusing the running mpv process means no window teardown. At 600s this caps drift to a couple seconds. config.example.yaml ships it at 600; README documents the knob and the "also lighten decode load" caveat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,6 +200,10 @@ func (d *Daemon) applyLayout(ctx context.Context, name string) error {
|
||||
d.wg.Add(1)
|
||||
go func() { defer d.wg.Done(); d.placeLoop(loCtx, places) }()
|
||||
}
|
||||
if secs := cfg.Player.ResyncSeconds; secs > 0 && len(players) > 0 {
|
||||
d.wg.Add(1)
|
||||
go func() { defer d.wg.Done(); d.resyncLoop(loCtx, players, time.Duration(secs)*time.Second) }()
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.players = players
|
||||
@@ -290,6 +294,38 @@ func (d *Daemon) placeLoop(ctx context.Context, places []placement) {
|
||||
}
|
||||
}
|
||||
|
||||
// resyncLoop reconnects one stream at a time to the live edge, cycling through
|
||||
// all of them so each is resynced roughly once per interval. Spreading the
|
||||
// reconnects (rather than doing them all at once) means only a single tile
|
||||
// ever blips, and it bounds the latency drift that live RTSP accumulates when
|
||||
// the Pi decodes slightly behind real-time.
|
||||
func (d *Daemon) resyncLoop(ctx context.Context, players []*player.Player, interval time.Duration) {
|
||||
step := interval / time.Duration(len(players))
|
||||
if step < time.Second {
|
||||
step = time.Second
|
||||
}
|
||||
ticker := time.NewTicker(step)
|
||||
defer ticker.Stop()
|
||||
i := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
p := players[i%len(players)]
|
||||
i++
|
||||
if !p.Running() {
|
||||
continue
|
||||
}
|
||||
if err := p.Reload(); err != nil {
|
||||
d.log.Debug("resync failed", "slot", p.Slot, "camera", p.Name, "err", err)
|
||||
continue
|
||||
}
|
||||
d.log.Debug("resynced stream to live edge", "slot", p.Slot, "camera", p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// sleepCtx sleeps for d or until ctx is cancelled; false means cancelled.
|
||||
func sleepCtx(ctx context.Context, dur time.Duration) bool {
|
||||
t := time.NewTimer(dur)
|
||||
|
||||
Reference in New Issue
Block a user