Place windows at map time so stream restarts never flash
Diagnosis: individual tiles flickered when their stream died and mpv relaunched (confirmed via status: a PID dropping to 0 and coming back). The restarted window mapped at the video's native size wherever sway dropped it, and the corrective loop only snapped it into its cell on the next tick — up to 2s later on the relaxed cadence. Fix, at the source instead of racing the map: - Pre-install a per-slot for_window rule (matched on each mpv's unique window title, anchored so slot-1 never matches slot-10) so sway positions and sizes the window synchronously the moment it maps. - Pass --geometry=WxH per tile so mpv opens at the tile size rather than resizing itself to the video's native size on load. The placeLoop remains as a corrective backstop for mid-life drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,3 +185,17 @@ func (c *Client) Place(ctx context.Context, pid int, r Rect) error {
|
|||||||
func (c *Client) PrepareForMPV(ctx context.Context) error {
|
func (c *Client) PrepareForMPV(ctx context.Context) error {
|
||||||
return c.run(ctx, `for_window [app_id="mpv"] floating enable, border none`)
|
return c.run(ctx, `for_window [app_id="mpv"] floating enable, border none`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PlaceOnMap installs a for_window rule that positions any window with the
|
||||||
|
// given title at rect the moment it maps. This is what keeps a restarting
|
||||||
|
// stream from flashing at the wrong place/size: sway applies the geometry
|
||||||
|
// synchronously at map time, before the daemon's corrective loop ever sees
|
||||||
|
// the window. Re-installing a rule for the same title replaces it. The title
|
||||||
|
// is anchored (^...$) so slot-1 never matches slot-10.
|
||||||
|
func (c *Client) PlaceOnMap(ctx context.Context, title string, r Rect) error {
|
||||||
|
cmd := fmt.Sprintf(
|
||||||
|
`for_window [title="^%s$"] floating enable, border none, move absolute position %d %d, resize set %d %d`,
|
||||||
|
title, r.X, r.Y, r.W, r.H,
|
||||||
|
)
|
||||||
|
return c.run(ctx, cmd)
|
||||||
|
}
|
||||||
|
|||||||
@@ -177,6 +177,13 @@ func (d *Daemon) applyLayout(ctx context.Context, name string) error {
|
|||||||
|
|
||||||
for _, s := range specs {
|
for _, s := range specs {
|
||||||
p := player.New(s.slot, s.name, s.url, cfg.Player, d.runDir, d.log)
|
p := player.New(s.slot, s.name, s.url, cfg.Player, d.runDir, d.log)
|
||||||
|
p.TileW, p.TileH = s.rect.W, s.rect.H
|
||||||
|
// Pre-install the placement rule so sway positions this slot's window
|
||||||
|
// (including after mid-life mpv restarts) the moment it maps — no
|
||||||
|
// wrong-place flash while waiting for the corrective loop.
|
||||||
|
if err := d.comp.PlaceOnMap(ctx, p.Title(), s.rect); err != nil {
|
||||||
|
d.log.Warn("could not preinstall placement rule", "slot", s.slot, "err", err)
|
||||||
|
}
|
||||||
players = append(players, p)
|
players = append(players, p)
|
||||||
places = append(places, placement{p: p, rect: s.rect})
|
places = append(places, placement{p: p, rect: s.rect})
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ type Player struct {
|
|||||||
Slot int // grid cell index (row-major)
|
Slot int // grid cell index (row-major)
|
||||||
Name string // camera name, for logs/titles
|
Name string // camera name, for logs/titles
|
||||||
URL string // RTSP(S) source
|
URL string // RTSP(S) source
|
||||||
|
// TileW/TileH, when set (before Supervise), are passed as --geometry so
|
||||||
|
// mpv opens its window at the tile size instead of the video's native
|
||||||
|
// size — a restarting stream then maps already-sized for its cell.
|
||||||
|
TileW, TileH int
|
||||||
|
|
||||||
cfg config.Player
|
cfg config.Player
|
||||||
ipcPath string
|
ipcPath string
|
||||||
@@ -94,6 +98,12 @@ func (p *Player) args() []string {
|
|||||||
// the audio decoder saves CPU per stream on the Pi.
|
// the audio decoder saves CPU per stream on the Pi.
|
||||||
args = append(args, "--no-audio")
|
args = append(args, "--no-audio")
|
||||||
}
|
}
|
||||||
|
if p.TileW > 0 && p.TileH > 0 {
|
||||||
|
// Open the window at the tile size; --geometry overrides mpv's
|
||||||
|
// resize-to-video-size on load, so the compositor never has to snap
|
||||||
|
// the window back into its cell.
|
||||||
|
args = append(args, fmt.Sprintf("--geometry=%dx%d", p.TileW, p.TileH))
|
||||||
|
}
|
||||||
if p.cfg.MaxFPS > 0 {
|
if p.cfg.MaxFPS > 0 {
|
||||||
args = append(args, fmt.Sprintf("--vf=fps=%d", p.cfg.MaxFPS))
|
args = append(args, fmt.Sprintf("--vf=fps=%d", p.cfg.MaxFPS))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user