Make video wallpapers survive first contact
Shipping met reality tonight, and reality won three rounds before we did. mpvpaper's -f forks it into the background, which a supervisor reads as instant death — the respawn loop repainted the desktop black once a second. The reaper's own kills fired onExited like crashes, so the supervisor ate its young until deliberate deaths got marked. And the video's path shared wallpaperPath with the still pipeline, whose transactional persistence clobbered it — it now lives under its own videoWallpaperPath key, read by the lock screen and the doctor too. Restore is the service's own now, reactive and once per session: a one-shot timer raced the async preference load and availability probe at cold start and silently lost. A video-wallpaper IPC target drives start/stop/pause from a terminal and from the contract sweep to come. Verified live: one player per output, same PID across restarts, VAAPI engaged, 0.2% CPU steady for 1440p30 h264, pause from the bar pill. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -50,6 +50,9 @@ Singleton {
|
||||
readonly property string framePath: root.frameDir + "/video-wallpaper-frame.png"
|
||||
|
||||
readonly property var outputs: Quickshell.screens.map(screen => screen.name)
|
||||
// A string, because the array above is a fresh identity every evaluation
|
||||
// and respawning on identity churn stacked players on the same output.
|
||||
readonly property string outputSignature: root.outputs.join(",")
|
||||
|
||||
function videoDir(): string {
|
||||
const stored = String(DesktopPreferences.get("videoWallpaperDir") || "Videos/Wallpapers");
|
||||
@@ -101,8 +104,11 @@ Singleton {
|
||||
// One mpvpaper per output. `-p -a FULL` is belt-and-braces auto-pause;
|
||||
// the real pausing happens over the IPC socket below. `hwdec=vaapi` is
|
||||
// named explicitly so a silent software-decode fallback cannot hide.
|
||||
// No `-f`: that flag forks mpvpaper into the background, which reads to a
|
||||
// supervisor as the process dying instantly — it must stay foreground so
|
||||
// onExited means what it says.
|
||||
function mpvpaperCommand(output: string): var {
|
||||
return ["mpvpaper", "-f", "-o",
|
||||
return ["mpvpaper", "-o",
|
||||
"hwdec=vaapi profile=fast no-audio loop-file=inf panscan=1.0 "
|
||||
+ "input-ipc-server=" + root.socketFor(output),
|
||||
"-p", "-a", "FULL",
|
||||
@@ -132,11 +138,17 @@ Singleton {
|
||||
function stop(): void {
|
||||
if (!root.active)
|
||||
return;
|
||||
root.restoreConsumed = true;
|
||||
root.path = "";
|
||||
root.manuallyPaused = false;
|
||||
playerRespawn.stop();
|
||||
for (const player of root.players)
|
||||
spawnDelay.stop();
|
||||
for (const player of root.players) {
|
||||
player.retiring = true;
|
||||
player.running = false;
|
||||
}
|
||||
root.players = [];
|
||||
reaper.running = true;
|
||||
hyprpaperControl.command = ["systemctl", "--user", "start", "hyprpaper.service"];
|
||||
hyprpaperControl.running = true;
|
||||
Qt.callLater(() => Wallpaper.refreshActive());
|
||||
@@ -152,18 +164,45 @@ Singleton {
|
||||
onTriggered: root.spawnPlayers()
|
||||
}
|
||||
|
||||
// Reap-then-spawn, always in that order. Every mpvpaper on this session
|
||||
// is Panama's, so a blanket pkill is the reliable way to guarantee one
|
||||
// player per output — politely asking Process objects to stop proved
|
||||
// racy: a replaced object could orphan its child, and three players once
|
||||
// stacked on one output.
|
||||
function spawnPlayers(): void {
|
||||
for (const player of root.players)
|
||||
// Retiring marks a deliberate death: the reaper's kill fires each
|
||||
// player's onExited exactly like a crash would, and without the mark
|
||||
// the supervisor respawned in response to its own reaping, forever.
|
||||
for (const player of root.players) {
|
||||
player.retiring = true;
|
||||
player.running = false;
|
||||
const spawned = [];
|
||||
for (const output of root.outputs) {
|
||||
const player = playerComponent.createObject(root, { output: output });
|
||||
player.command = root.mpvpaperCommand(output);
|
||||
player.running = true;
|
||||
spawned.push(player);
|
||||
}
|
||||
root.players = spawned;
|
||||
pauseSync.restart();
|
||||
root.players = [];
|
||||
reaper.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: reaper
|
||||
command: ["pkill", "-x", "mpvpaper"]
|
||||
onExited: spawnDelay.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: spawnDelay
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
if (!root.active)
|
||||
return;
|
||||
const spawned = [];
|
||||
for (const output of root.outputs) {
|
||||
const player = playerComponent.createObject(root, { output: output });
|
||||
player.command = root.mpvpaperCommand(output);
|
||||
player.running = true;
|
||||
spawned.push(player);
|
||||
}
|
||||
root.players = spawned;
|
||||
pauseSync.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
@@ -172,17 +211,20 @@ Singleton {
|
||||
Process {
|
||||
id: player
|
||||
property string output: ""
|
||||
property bool retiring: false
|
||||
onExited: {
|
||||
// A dead player while a video is meant to be active is a
|
||||
// crash (mpvpaper has a known hotplug segfault): respawn the
|
||||
// whole set after a beat rather than reasoning per-output.
|
||||
if (root.active && !playerRespawn.running)
|
||||
// A retiring player died because the supervisor killed it —
|
||||
// reacting to that is how the reap loop once ate its young.
|
||||
if (!player.retiring && root.active && !playerRespawn.running)
|
||||
playerRespawn.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOutputsChanged: if (root.active) playerRespawn.restart()
|
||||
onOutputSignatureChanged: if (root.active) playerRespawn.restart()
|
||||
|
||||
Process {
|
||||
id: hyprpaperControl
|
||||
@@ -240,5 +282,32 @@ Singleton {
|
||||
root.manuallyPaused = !root.manuallyPaused;
|
||||
}
|
||||
|
||||
Component.onCompleted: root.rescan()
|
||||
// ── Startup restore ─────────────────────────────────────────────────────
|
||||
// The service restores its own video, reactively: at cold start the
|
||||
// preferences file and the mpvpaper probe both land asynchronously, so a
|
||||
// one-shot timer (the still pipeline's approach) raced them and lost.
|
||||
// Once per session — a user's stop() is not to be overridden.
|
||||
property bool restoreConsumed: false
|
||||
|
||||
function tryRestore(): void {
|
||||
if (root.restoreConsumed || root.active || !root.available)
|
||||
return;
|
||||
const video = String(DesktopPreferences.get("videoWallpaperPath") || "");
|
||||
if (!root.isVideo(video))
|
||||
return;
|
||||
root.restoreConsumed = true;
|
||||
root.start(video);
|
||||
}
|
||||
|
||||
onAvailableChanged: root.tryRestore()
|
||||
|
||||
Connections {
|
||||
target: DesktopPreferences
|
||||
function onRevisionChanged(): void { root.tryRestore(); }
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.rescan();
|
||||
root.tryRestore();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user