Search code examples
pythonmacospyobjc

PyObjC: Accessing MPNowPlayingInfoCenter


I am creating a MacOS media player in Python (version 3.10) and want to connect it to the MacOS "Now Playing" status.

I have worked with PyObjC a bit in order to listen for media key events, but have not been able to connect to the MPNowPlayingInfoCenter interface. According to the MPNowPlayingInfoCenter documentation I need access to a shared instance via the default() method, however the method MediaPlayer.MPNowPlayingInfoCenter.default() does not exist.

Does anyone have a starting point for Now Playing functionality via PyObjC?


Solution

  • In case anyone finds this question seeking to add 'Now Playing' functionality to a python application on MacOS, below is example code for connection to both update now playing information and receive commands from the operating system.

    Note also that this code will receive input from keyboard media keys without the need for an event tap.

    I found this code eliminated many odd behaviors in my music player and would recommend this route for all music player applications on MacOS.

    from AppKit import NSImage
    from AppKit import NSMakeRect
    from AppKit import NSCompositingOperationSourceOver
    from Foundation import NSMutableDictionary
    from MediaPlayer import MPNowPlayingInfoCenter
    from MediaPlayer import MPRemoteCommandCenter
    from MediaPlayer import MPMediaItemArtwork
    from MediaPlayer import MPMediaItemPropertyTitle
    from MediaPlayer import MPMediaItemPropertyArtist
    from MediaPlayer import MPMediaItemPropertyPlaybackDuration
    from MediaPlayer import MPMediaItemPropertyArtwork
    from MediaPlayer import MPMusicPlaybackState
    from MediaPlayer import MPMusicPlaybackStatePlaying
    from MediaPlayer import MPMusicPlaybackStatePaused
    
    
    class MacNowPlaying:
        def __init__(self):
            # Get the Remote Command center
            # ... which is how the OS sends commands to the application
            self.cmd_center = MPRemoteCommandCenter.sharedCommandCenter()
    
            # Get the Now Playing Info Center
            # ... which is how this application notifies MacOS of what is playing
            self.info_center = MPNowPlayingInfoCenter.defaultCenter()
    
            # Enable Commands
            self.cmd_center.playCommand()           .addTargetWithHandler_(self.hPlay)
            self.cmd_center.pauseCommand()          .addTargetWithHandler_(self.hPause)
            self.cmd_center.togglePlayPauseCommand().addTargetWithHandler_(self.hTogglePause)
            self.cmd_center.nextTrackCommand()      .addTargetWithHandler_(self.hNextTrack)
            self.cmd_center.previousTrackCommand()  .addTargetWithHandler_(self.hPrevTrack)
    
        def hPlay(self, event):
            """
            Handle an external 'playCommand' event
            """
            ...
            return 0
    
        def hPause(self, event):
            """
            Handle an external 'pauseCommand' event
            """
            ...
            return 0
    
        def hTogglePause(self, event):
            """
            Handle an external 'togglePlayPauseCommand' event
            """
            ...
            return 0
    
        def hNextTrack(self, event):
            """
            Handle an external 'nextTrackCommand' event
            """
            ...
            return 0
    
        def hPrevTrack(self, event):
            """
            Handle an external 'previousTrackCommand' event
            """
            ...
            return 0
    
        def onStopped(self):
            """
            Call this method to update 'Now Playing' state to: stopped
            """
            self.info_center.setPlaybackState_(MPMusicPlaybackStateStopped)
            return 0
    
        def onPaused(self):
            """
            Call this method to update 'Now Playing' state to: paused
            """
            self.info_center.setPlaybackState_(MPMusicPlaybackStatePaused)
            return 0
    
        def onPlaying(self, title: str, artist: str, length, int, cover: bytes = None):
            """
            Call this method to update 'Now Playing' state to: playing
    
            :param title: Track Title
            :param artist: Track Artist
            :param length: Track Length
            :param cover: Track cover art as byte array
            """
    
            nowplaying_info = NSMutableDictionary.dictionary()
    
            # Set basic track information
            nowplaying_info[MPMediaItemPropertyTitle]            = title
            nowplaying_info[MPMediaItemPropertyArtist]           = artist
            nowplaying_info[MPMediaItemPropertyPlaybackDuration] = length
    
            # Set the cover art
            # ... which requires creation of a proper MPMediaItemArtwork object
            cover = ptr.record.cover
            if cover is not None:
                # Apple documentation on how to load and set cover artwork is less than clear
                # The below code was cobbled together from numerous sources
                # ... REF: https://stackoverflow.com/questions/11949250/how-to-resize-nsimage/17396521#17396521
                # ... REF: https://developer.apple.com/documentation/mediaplayer/mpmediaitemartwork?language=objc
                # ... REF: https://developer.apple.com/documentation/mediaplayer/mpmediaitemartwork/1649704-initwithboundssize?language=objc
    
                img = NSImage.alloc().initWithData_(cover)
    
                def resize(size):
                    new = NSImage.alloc().initWithSize_(size)
                    new.lockFocus()
                    img.drawInRect_fromRect_operation_fraction_(
                        NSMakeRect(0, 0, size.width, size.height),
                        NSMakeRect(0, 0, img.size().width, img.size().height),
                        NSCompositingOperationSourceOver,
                        1.0,
                    )
                    new.unlockFocus()
                    return new
                art = MPMediaItemArtwork.alloc().initWithBoundsSize_requestHandler_(img.size(), resize)
    
                nowplaying_info[MPMediaItemPropertyArtwork] = art
    
            # Set the metadata information for the 'Now Playing' service
            self.info_center.setNowPlayingInfo_(nowplaying_info)
    
            # Set the 'Now Playing' state to: playing
            self.info_center.setPlaybackState_(MPMusicPlaybackStatePlaying)
    
            return 0
    

    The above code was simplified from my own project as an illustration of the connection to MacOS via PyObjC. This is a partial implementation of functionality provided by Apple designed to support only basic 'Now Playing' information center interaction. Tested on macOS Monterey (12.4).