Files
RTSP-Streamer/internal/ipc/ipc.go
Levi Woodard 98ddf1645c Smoother daemon: no-op reloads, batched placement, muted audio, graceful teardown
- 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>
2026-07-02 07:38:01 -05:00

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
}