- Skip stream restarts when a reload resolves to the identical wall (same URLs, tile geometry, player settings) — saving an unrelated config edit no longer blanks the screen. - Replace per-tile placement polling (a swaymsg fork per tile per second, plus 150ms get_tree polling per tile at startup) with one shared loop: a single get_tree snapshot per tick, re-placing only drifted windows, relaxing to a 2s tick once settled. - Mute streams by default (--no-audio) to skip an audio decoder per stream; opt back in with player.audio: true. - Graceful, parallel mpv teardown via cmd.Cancel/WaitDelay, fixing the double-Wait race between Stop and Supervise and cutting worst-case layout switches from ~2s x N streams to ~2s total. - status: probe mpv IPC outside the daemon mutex so a slow probe can't block layout switches. - View sync: reuse the Protect session across ticks (re-login only on expiry) and pick up view_refresh_seconds changes without a restart. - Health strikes keyed by player, not slot, so they never carry across layout switches; prune stale entries. - New `rtsp-streamer reload` CLI; fix dead sort in `layout ls`; back off on persistent control-socket accept errors; fsync config before the atomic rename (SD-card power-cut safety); bump IPC client deadline; gofmt stragglers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
2.2 KiB
Go
78 lines
2.2 KiB
Go
// Package ipc defines the tiny control protocol shared by the daemon (server)
|
|
// and the CLI/TUI (clients). Messages are newline-delimited JSON over a unix
|
|
// socket, so switching layouts or reading status never requires restarting
|
|
// the video wall.
|
|
package ipc
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// Request is a command sent to the daemon.
|
|
type Request struct {
|
|
Cmd string `json:"cmd"` // "status" | "reload" | "set-layout"
|
|
Name string `json:"name,omitempty"` // layout name for set-layout
|
|
}
|
|
|
|
// SlotStatus reports one grid cell's state.
|
|
type SlotStatus struct {
|
|
Slot int `json:"slot"`
|
|
Camera string `json:"camera"`
|
|
PID int `json:"pid"`
|
|
Running bool `json:"running"`
|
|
Healthy bool `json:"healthy"`
|
|
}
|
|
|
|
// Response is the daemon's reply.
|
|
type Response struct {
|
|
OK bool `json:"ok"`
|
|
Error string `json:"error,omitempty"`
|
|
ActiveLayout string `json:"active_layout,omitempty"`
|
|
Slots []SlotStatus `json:"slots,omitempty"`
|
|
}
|
|
|
|
// SocketPath returns the control socket path inside the runtime dir.
|
|
func SocketPath() string {
|
|
return filepath.Join(RunDir(), "control.sock")
|
|
}
|
|
|
|
// RunDir is the per-user runtime directory for sockets, created on demand.
|
|
func RunDir() string {
|
|
base := os.Getenv("XDG_RUNTIME_DIR")
|
|
if base == "" {
|
|
base = filepath.Join(os.TempDir(), "rtsp-streamer-"+strconv.Itoa(os.Getuid()))
|
|
} else {
|
|
base = filepath.Join(base, "rtsp-streamer")
|
|
}
|
|
return base
|
|
}
|
|
|
|
// Send dials the daemon, sends one request, and returns the reply.
|
|
func Send(req Request) (*Response, error) {
|
|
conn, err := net.DialTimeout("unix", SocketPath(), 3*time.Second)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot reach daemon (is it running?): %w", err)
|
|
}
|
|
defer conn.Close()
|
|
// Generous deadline: a reload that rebuilds the wall tears down and
|
|
// respawns every mpv before replying, and status probes each stream's IPC.
|
|
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
|
|
enc := json.NewEncoder(conn)
|
|
if err := enc.Encode(req); err != nil {
|
|
return nil, err
|
|
}
|
|
var resp Response
|
|
if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|