TL;DR — Wallnetic 1.4.2 has six pause conditions and one rule above them: no lock, wake or power event resumes a pause the user asked for. Getting there took a 1.4.1 regression we shipped, a notification that could never fire and a watchdog that must tell frozen from paused.
A live wallpaper has many reasons to stand still — a lock, a saver, another user switching in, a fullscreen game. The hard question comes on resume: who paused it? Wallnetic answers with pause ownership. This is part 3 of our product notes, after what is new in Wallnetic 1.4 and SCORM and H5P in Blink AI.
Six flags, one verdict
Six conditions feed shouldBePaused; PowerManager keeps each as its own flag and recomputes the verdict on every read:
- Display sleep —
NSWorkspacenotifications - Screen saver — a cross-process notification
- Screen lock and fast user switching — one session flag, new in 1.4.1; video used to keep decoding behind the login window
- Low Power Mode — always pauses
- Battery power — a setting, on by default, with a prompt to keep playing
- A fullscreen app — a setting, on by default, found by a 2-second window poll
Who owns the pause
Nothing had ever recorded who paused. In 1.4.1 we let lock, wake and saver resumes bypass the “Auto-resume when conditions change” setting — nobody asks for a lock — which exposed the gap on every unlock: pause it yourself, lock, unlock, and it played again. Every time. In 1.4.2, PowerManager claims a pause (pausedByPower) only if the wallpaper was playing; Play or Pause hands ownership back through userDidTogglePlayback(). The pure PowerPauseOwnership type decides:
| Situation | Resumes? |
|---|---|
| You paused, then locked and unlocked | No — nothing was claimed |
| Lock, user switch, sleep or saver paused it, then cleared | Yes, even with auto-resume off |
| Battery, Low Power Mode or a fullscreen app paused it, then cleared | Yes, if auto-resume is on |
| Any condition still active | No |
The rule governs power and session events only. A wallpaper applied by hand, by a playlist or by time-of-day switching starts playing unless a condition blocks it.
The notification that could never fire
Screen-saver pausing predated 1.4.1 and never worked. Its observers sat on NotificationCenter.default, but com.apple.screensaver.didstart is a cross-process name that only arrives on DistributedNotificationCenter. In 1.4.1 we moved them there with .deliverImmediately — by default, distributed delivery is held back while the app is inactive, which a wallpaper app always is when the screen locks.
Notifications also get lost. Every wake, unlock and saver exit calls resyncSessionState(), which re-reads lock state from the window server (CGSessionCopyCurrentDictionary) instead of trusting the event. Since 1.4.2 it also runs at launch, for login items starting behind a locked screen.
Truthful state and the watchdog
- Truthful state. Until 1.4.2,
togglePlayback()marked the wallpaper playing before the power gate swallowed the request, so the menu bar said Playing over a still desktop. NowplaybackPlay()reports whether playback started, and the UI says so. - The watchdog. A wedged
AVPlayerkeeps showing its last frame, so a stall looks like a pause.DesktopWindowControllersamples playback every 5 seconds, but the verdict is the purePlaybackWatchdog.isFrozen: frozen only if playback is intended,shouldBePausedis false and the clock has not moved. A deliberatepause()stops the timer. - Rate, not just progress. Since 1.4.2 each tick also pins a drifting rate to 1.0 — no seek, which would jump back a keyframe.
What was hard
- A regression no test caught. The 1.4.1 bypass was right for locks and wrong for user pauses; we moved the decision into
PowerPauseOwnership, where that exact case is now a unit test. - A safety net with a hole. The watchdog stands down while paused, so a flag that never clears is a permanent freeze. Display sleep was the one flag no resync touched — and every resync ran from a resume handler, after the very notification it was meant to back up. We added a clear-only
CGDisplayIsAsleepcheck, including on the existing 2-second poll — no new timer. - Failing towards playing. During a normal wake a display can read as asleep for an instant, so the check only clears the flag, never sets it.
What this buys
A wallpaper that stops while the Mac is locked, asleep, behind a saver or switched away, comes back when you return and keeps your own pause through all of it. Six live flags, one ownership bit, a few unit-tested pure rules — the model is small. Next in product notes: what it took to stop Blink AI’s coach from saying “great job” to a wrong answer.
Frequently asked questions
Why does my live wallpaper stay paused after I unlock my Mac?
Either you paused it yourself or a pause condition is still active. Since 1.4.2, unlocking or waking resumes only pauses Wallnetic caused; yours holds until you press Play or a wallpaper is applied. Wallnetic also stays paused in Low Power Mode and, with the default settings, on battery power or behind a fullscreen app.
How does Wallnetic recover a frozen live wallpaper?
A playback watchdog checks each display’s playback clock every 5 seconds. If the clock has not moved on two checks in a row while playback should be running, Wallnetic recovers playback on that display by itself, without reopening the app. It stands down during deliberate pauses, so it never fights a lock or your own Pause.
Working on something similar?
T-Square architects, builds and operates production systems for learning, AI and custom software products. Talk to a senior engineer for a second opinion.
