From 1dffdd05f27540287f1eefb1c5185cf89a101332 Mon Sep 17 00:00:00 2001 From: Jis G Jacob Date: Thu, 14 May 2026 08:26:23 +0530 Subject: [PATCH] Atoll: freeze Spotify Canvas wallpaper when audio is paused SIGSTOP WallpaperAgent and the aerial extensions while MusicManager reports paused so the lockscreen Canvas holds its current frame, and SIGCONT on resume, hide(), and willTerminate so the agent is never left suspended. Signed-off-by: Jis G Jacob --- .../FullScreenArtworkWindowManager.swift | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/DynamicIsland/managers/FullScreenArtworkWindowManager.swift b/DynamicIsland/managers/FullScreenArtworkWindowManager.swift index f790e8b..ef80189 100644 --- a/DynamicIsland/managers/FullScreenArtworkWindowManager.swift +++ b/DynamicIsland/managers/FullScreenArtworkWindowManager.swift @@ -89,6 +89,14 @@ private final class LoopingVideoView: NSView { playerLayer?.removeFromSuperlayer() playerLayer = nil } + + func pause() { + queuePlayer?.pause() + } + + func resume() { + queuePlayer?.play() + } } private final class WallpaperTransitionImageView: NSView { @@ -304,6 +312,9 @@ final class FullScreenArtworkWindowManager: ObservableObject { private var trackChangeCancellable: AnyCancellable? private var artworkCacheCancellable: AnyCancellable? private var videoArtworkCancellable: AnyCancellable? + private var playbackStateCancellable: AnyCancellable? + private var hasSuspendedWallpaperAgent = false + private var appTerminationObserver: NSObjectProtocol? private var clickWindow: ClickReceiverWindow? private var clickWindowDelegated = false private var videoWindow: NSWindow? @@ -339,6 +350,8 @@ final class FullScreenArtworkWindowManager: ObservableObject { observeArtworkLayoutOverCanvasPreference() observeLyricsChanges() observeLyricsPreference() + observePlaybackStateChanges() + observeAppTermination() } func show(artwork: NSImage, videoURL: URL? = nil, allowLiveWallpaper: Bool = false) { @@ -356,6 +369,7 @@ final class FullScreenArtworkWindowManager: ObservableObject { func hide() { guard isShowing else { return } + resumeWallpaperAgentIfNeeded() isShowing = false isLiveWallpaperAllowed = false activeLiveWallpaperFingerprint = nil @@ -426,6 +440,89 @@ final class FullScreenArtworkWindowManager: ObservableObject { } } + private func observePlaybackStateChanges() { + playbackStateCancellable = MusicManager.shared.$isPlaying + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] isPlaying in + self?.applyCanvasPlaybackState(isPlaying: isPlaying) + } + } + + private static let wallpaperRenderProcessNames: [String] = [ + "WallpaperAgent", + "WallpaperAerialsExtension", + "WallpaperVideoExtension", + "wallpaperexportd" + ] + + private func applyCanvasPlaybackState(isPlaying: Bool) { + if let videoView, videoWindow != nil { + if isPlaying { + videoView.resume() + } else { + videoView.pause() + } + } + + let canSuspend = isShowing && isLiveWallpaperAllowed && activeLiveWallpaperFingerprint != nil + print("[FullScreenArtworkWindowManager] playback state -> isPlaying=\(isPlaying) showing=\(isShowing) liveAllowed=\(isLiveWallpaperAllowed) fingerprint=\(activeLiveWallpaperFingerprint ?? "nil")") + + if isPlaying { + resumeWallpaperAgentIfNeeded() + } else if canSuspend { + suspendWallpaperAgent() + } + } + + private func suspendWallpaperAgent() { + guard !hasSuspendedWallpaperAgent else { return } + var anySucceeded = false + for name in Self.wallpaperRenderProcessNames { + if signalProcess(name: name, signalFlag: "-STOP") { + anySucceeded = true + } + } + hasSuspendedWallpaperAgent = anySucceeded + print("[FullScreenArtworkWindowManager] SIGSTOP wallpaper processes -> anySucceeded=\(anySucceeded)") + } + + private func resumeWallpaperAgentIfNeeded() { + guard hasSuspendedWallpaperAgent else { return } + for name in Self.wallpaperRenderProcessNames { + _ = signalProcess(name: name, signalFlag: "-CONT") + } + hasSuspendedWallpaperAgent = false + print("[FullScreenArtworkWindowManager] SIGCONT wallpaper processes") + } + + @discardableResult + private func signalProcess(name: String, signalFlag: String) -> Bool { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/killall") + task.arguments = [signalFlag, name] + task.standardError = Pipe() + task.standardOutput = Pipe() + do { + try task.run() + task.waitUntilExit() + return task.terminationStatus == 0 + } catch { + print("[FullScreenArtworkWindowManager] killall \(signalFlag) \(name) threw: \(error)") + return false + } + } + + private func observeAppTermination() { + appTerminationObserver = NotificationCenter.default.addObserver( + forName: NSApplication.willTerminateNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.resumeWallpaperAgentIfNeeded() + } + } + private func applyPresentation( artwork: NSImage, videoURL: URL?, @@ -1447,6 +1544,7 @@ final class FullScreenArtworkWindowManager: ObservableObject { activeVideoWindowURL = videoURL } window.orderFrontRegardless() + applyCanvasPlaybackState(isPlaying: MusicManager.shared.isPlaying) } private func scheduleHideVideoWindow(after delay: Duration, expectedURL: URL?) {