Search code examples
swiftscreensaver

Audio keeps playing after screensaver ends


I'm working on a screen saver with (optional) audio. But whenever i deactivate it by swiping the mouse, the audio keeps playing for a few more seconds. This does not happen when clicking preview in sys preferences though. My theory is that when moving the mouse in preview, the screensaver process gets instantly killed, but not when it's not running it as preview. Is there a way i can sense mouse activity and stop the audio myself by running a function? I have supplied some code below: https://gist.github.com/MaxTechnics/3d4280fabc4da53b6df1022864d1bf23 Will provide more if requested. Thanks in advance!

Updated: this is what i think is the main part of the problem since the audio never stops in time

// MARK: - Lifecycle
extension VideoView {
    
    override func startAnimation() {
        super.startAnimation()
        manager.player.play()
    }
    
    override func stopAnimation() {
        super.stopAnimation()
        manager.player.pause()
    }
}

Solution

  • After some fiddling, while we can't intercept keyboard (and shouldn't to reliably detect a quit), it's actually possible to listen to some distributed notifications in the screensaver that tells us when the user gets logged in, and use that to call our code to stop animations/sound, as there's a sizable lag between the moment we are effectively killed and the moment the user gets back to desktop.

    I did find some events in this answer here : Monitoring Screensaver Events in OSX and while it's a bit dated those events are still fired and can be intercepted inside the screensaver despite the sandboxing. I did found a few new ones though that may be interesting for other purposes.

    Anyway, here's what's working for me :

    class AerialView: ScreenSaverView {
        ...
        func setNotifications {
            DistributedNotificationCenter.default.addObserver(self, selector: #selector(AerialView.willStop(_:)), name: Notification.Name("com.apple.screensaver.willstop"), object: nil)
        }
    
        @objc func willStop(_ aNotification: Notification) {
            NSLog("############ willStop")
            // Put your stop audio/animation code here
        }
    }
    

    (you will have to call setNotifications yourself or put that code somewhere else).

    In practice, there are multiple events that are fired one after the other when the screensaver exits:

    • com.apple.screensaver.willstop
    • com.apple.screensaver.didstop
    • com.apple.screenIsUnlocked
    • com.apple.screenLockUIIsShown

    Here's some logging of those events

    2021-04-27 14:52:00.564 : ############ screenLockUIIsShown
    2021-04-27 14:52:01.873 : ############ screenIsUnlocked
    2021-04-27 14:52:01.873 : ############ willStop
    2021-04-27 14:52:01.879 : ############ didStop
    

    The first event was when I pressed a key, about 1.5s later the watch unlock unlocked the screen and you can see the 3 events being fired in succession.

    The actual termination of the screensaver only happened at 14:52:07 in that case.

    For completeness, some new events to detect when the login UI is up and dismissed (could be useful for some other things). This is in 11.4 and I have no idea if those events are fired in previous versions of macOS. In firing order :

    • com.apple.shieldWindowRaised
    • com.apple.screenIsLocked
    • com.apple.screenLockUIIsShown
    • com.apple.screenLockUIIsHidden
    • com.apple.shieldWindowLowered
    • com.apple.screenIsUnlocked