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