PyObjC: Accessing MPNowPlayingInfoCenter

Question:

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?

Asked By: othalan

||

Answers:

The factory method for getting the default center is named “defaultCenter” in Objective-C, and that’s the name you can use in Python as well.

https://developer.apple.com/documentation/mediaplayer/mpnowplayinginfocenter?language=objc

Answered By: Ronald Oussoren

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).

Answered By: othalan
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.