diff --git a/backend/mpmedia.go b/backend/mpmedia.go index 521bef3..385a113 100644 --- a/backend/mpmedia.go +++ b/backend/mpmedia.go @@ -1,70 +1,12 @@ package backend -/** -* This file handles implementation of MacOS native controls via the native 'MediaPlayer' framework -**/ - -// #cgo CFLAGS: -x objective-c -// #cgo LDFLAGS: -framework Cocoa -framework MediaPlayer -// #include "mpmediabridge.h" import ( - "C" -) - -import ( - "fmt" - "unsafe" - - "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/player" ) +// MPMediaHandler is the handler for MacOS media controls and system events. type MPMediaHandler struct { player *player.Player playbackManager *PlaybackManager ArtURLLookup func(trackID string) (string, error) } - -func NewMPMediaHandler(player *player.Player, playbackManager *PlaybackManager) *MPMediaHandler { - mp := &MPMediaHandler{ - player: player, - playbackManager: playbackManager, - } - - mp.playbackManager.OnSongChange(func(track, _ *mediaprovider.Track) { - if mp.ArtURLLookup != nil { - mp.ArtURLLookup(track.CoverArtID) - } - - cTitle := C.CString(track.Name) - defer C.free(unsafe.Pointer(cTitle)) - - artist := "" - if len(track.ArtistNames) > 0 { - artist = track.ArtistNames[0] - } - - cArtist := C.CString(artist) - defer C.free(unsafe.Pointer(cArtist)) - - // TODO: pass in local URL for image (can be loaded via NSImage) - C.setNowPlayingInfo(cTitle, cArtist) - }) - - mp.player.OnStopped(func() { - // TODO: media stopped playing - fmt.Println("Stopped") - }) - - mp.player.OnPlaying(func() { - // TODO: media playing - fmt.Println("Playing") - }) - - mp.player.OnPaused(func() { - // TODO: media paused - fmt.Println("Paused") - }) - - return mp -} diff --git a/backend/mpmedia_mac.go b/backend/mpmedia_mac.go new file mode 100644 index 0000000..7855ed2 --- /dev/null +++ b/backend/mpmedia_mac.go @@ -0,0 +1,161 @@ +//go:build darwin + +package backend + +/** +* This file handles implementation of MacOS native controls via the native 'MediaPlayer' framework +**/ + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Cocoa -framework MediaPlayer +#include "mpmediabridge.h" +*/ +import ( + "C" +) + +import ( + "log" + "unsafe" + + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/player" +) + +// os_remote_command_callback is called by Objective-C when incoming OS media commands are received. +// +//export os_remote_command_callback +func os_remote_command_callback(command C.Command) { + switch command { + case C.PLAY: + mpMediaEventRecipient.OnCommandPlay() + case C.PAUSE: + mpMediaEventRecipient.OnCommandPause() + case C.STOP: + mpMediaEventRecipient.OnCommandStop() + case C.TOGGLE: + mpMediaEventRecipient.OnCommandTogglePlayPause() + case C.PREVIOUS_TRACK: + mpMediaEventRecipient.OnCommandPreviousTrack() + case C.NEXT_TRACK: + mpMediaEventRecipient.OnCommandNextTrack() + default: + log.Printf("unknown OS command received: %v", command) + } +} + +// global recipient for Object-C callbacks from command center. +// This is global so that it can be called from 'os_remote_command_callback' to avoid passing Go pointers into C. +var mpMediaEventRecipient *MPMediaHandler + +// NewMPMediaHandler creates a new MPMediaHandler instances and sets it as the current recipient +// for incoming system events. +func NewMPMediaHandler(player *player.Player, playbackManager *PlaybackManager) *MPMediaHandler { + mp := &MPMediaHandler{ + player: player, + playbackManager: playbackManager, + } + + // register remote commands and set callback target + mpMediaEventRecipient = mp + C.register_os_remote_commands() + + mp.playbackManager.OnSongChange(func(track, _ *mediaprovider.Track) { + if track != nil && track.ID != "" { + var artURL string + if mp.ArtURLLookup != nil { + var err error + if artURL, err = mp.ArtURLLookup(track.CoverArtID); err != nil { + log.Printf("error fetching art url: %s", err.Error()) + } + } + + cTitle := C.CString(track.Name) + defer C.free(unsafe.Pointer(cTitle)) + + var artist string + if len(track.ArtistNames) > 0 { + artist = track.ArtistNames[0] + } + + cArtist := C.CString(artist) + defer C.free(unsafe.Pointer(cArtist)) + + cArtURL := C.CString(artURL) + defer C.free(unsafe.Pointer(cArtURL)) + + C.set_os_now_playing_info(cTitle, cArtist, cArtURL) + } + }) + + mp.player.OnStopped(func() { + C.set_os_playback_state_stopped() + }) + + mp.player.OnPlaying(func() { + C.set_os_playback_state_playing() + }) + + mp.player.OnPaused(func() { + C.set_os_playback_state_paused() + }) + + return mp +} + +/** +* Handle incoming OS commands. +**/ + +// MPMediaHandler instance received OS command 'pause' +func (mp *MPMediaHandler) OnCommandPause() { + if mp == nil || mp.player == nil { + return + } + mp.player.Pause() +} + +// MPMediaHandler instance received OS command 'play' +func (mp *MPMediaHandler) OnCommandPlay() { + if mp == nil || mp.player == nil { + return + } + mp.player.Continue() +} + +// MPMediaHandler instance received OS command 'stop' +func (mp *MPMediaHandler) OnCommandStop() { + if mp == nil || mp.player == nil { + return + } + mp.player.Stop() +} + +// MPMediaHandler instance received OS command 'toggle' +func (mp *MPMediaHandler) OnCommandTogglePlayPause() { + if mp == nil || mp.player == nil { + return + } + if mp.player.GetStatus().State == player.Playing { + mp.OnCommandPause() + } else { + mp.OnCommandPlay() + } +} + +// MPMediaHandler instance received OS command 'next track' +func (mp *MPMediaHandler) OnCommandNextTrack() { + if mp == nil || mp.player == nil { + return + } + mp.player.SeekNext() +} + +// MPMediaHandler instance received OS command 'previous track' +func (mp *MPMediaHandler) OnCommandPreviousTrack() { + if mp == nil || mp.player == nil { + return + } + mp.player.SeekBackOrPrevious() +} diff --git a/backend/mpmedia_other.go b/backend/mpmedia_other.go new file mode 100644 index 0000000..1c0c275 --- /dev/null +++ b/backend/mpmedia_other.go @@ -0,0 +1,10 @@ +//go:build !darwin + +package backend + +import "github.com/dweymouth/supersonic/player" + +func NewMPMediaHandler(player *player.Player, playbackManager *PlaybackManager) *MPMediaHandler { + // MPMediaHandler only supports macOS. + return nil +} diff --git a/backend/mpmediabridge.h b/backend/mpmediabridge.h index 80b32e8..b3902a3 100644 --- a/backend/mpmediabridge.h +++ b/backend/mpmediabridge.h @@ -1,18 +1,46 @@ /** * mpmediabridge.h * - * This file provides a C bridge to the Objective-C framework for macOS media playback. + * This file provides a C bridge to the Objective-C framework for macOS media playback & events. * It offers a simplified interface to interact with the MPNowPlayingInfoCenter and other - * related media playback functionalities in macOS without dealing directly with - * Objective-C code. + * related media playback functionalities in macOS without dealing directly with Objective-C code. */ #include #include +/** +* OS remote command enumeration, accepted by 'os_remote_command_callback'. +*/ +typedef enum { + PLAY, + PAUSE, + STOP, + TOGGLE, + NEXT_TRACK, + PREVIOUS_TRACK +} Command; + +/** +* registers the 'os_remote_command_callback' to receive OS media commands. +*/ +void register_os_remote_commands(); + +/** +* Go-backed callback to static function that is called when OS remote commands are received. +*/ +void os_remote_command_callback(Command command); + /** * Updates the "Now Playing" information on macOS for media playback * using the MPNowPlayingInfoCenter API to set the metadata * for the currently playing media in the system's "Now Playing" interface. */ -void setNowPlayingInfo(const char *title, const char *artist); +void set_os_now_playing_info(const char *title, const char *artist, const char *coverArtFileURL); + +/** + * Setter functions for updating the global playback state. + */ +void set_os_playback_state_playing(); +void set_os_playback_state_paused(); +void set_os_playback_state_stopped(); diff --git a/backend/mpmediabridge.m b/backend/mpmediabridge.m index d8fce4c..4bf538d 100644 --- a/backend/mpmediabridge.m +++ b/backend/mpmediabridge.m @@ -1,47 +1,83 @@ #import "mpmediabridge.h" /** - * Native Objective-C function for setting "Now Playing" information on macOS for media playback using the native APIs. + * C bridge registering callbacks for media playback events using the native CommandCenter API. */ -void setNowPlayingInfoNative(const char *title, const char *artist, double trackDuration, NSImage *artworkImage, double elapsedTime) { - - // TODO: the remove command center logic needs to move +void register_os_remote_commands() { MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; [commandCenter.playCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - // TODO: Handle play command + os_remote_command_callback(PLAY); return MPRemoteCommandHandlerStatusSuccess; }]; [commandCenter.pauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - // TODO: Handle pause command + os_remote_command_callback(PAUSE); return MPRemoteCommandHandlerStatusSuccess; }]; [commandCenter.togglePlayPauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - // TODO: Handle toggle command + os_remote_command_callback(TOGGLE); return MPRemoteCommandHandlerStatusSuccess; }]; - MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter]; + [commandCenter.stopCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { + os_remote_command_callback(STOP); + return MPRemoteCommandHandlerStatusSuccess; + }]; + [commandCenter.nextTrackCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { + os_remote_command_callback(NEXT_TRACK); + return MPRemoteCommandHandlerStatusSuccess; + }]; + + [commandCenter.previousTrackCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { + os_remote_command_callback(PREVIOUS_TRACK); + return MPRemoteCommandHandlerStatusSuccess; + }]; +} + +/** + * C bridge setting "Now Playing" information on macOS for media playback using the native APIs. + */ +void set_os_now_playing_info(const char *title, const char *artist, const char *coverArtFileURL) { + NSString *coverArtLocationString = [NSString stringWithUTF8String:coverArtFileURL]; + NSURL *coverArtURL = [NSURL URLWithString:coverArtLocationString]; + NSImage *coverArtImage = [[NSImage alloc] initWithContentsOfURL:coverArtURL]; + + MPMediaItemArtwork *coverArt = [[MPMediaItemArtwork alloc] initWithBoundsSize:coverArtImage.size requestHandler:^NSImage * _Nonnull(CGSize size) { + return coverArtImage; + }]; + + MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter]; NSDictionary *nowPlayingInfo = @{ MPMediaItemPropertyTitle: [NSString stringWithUTF8String:title], MPMediaItemPropertyArtist: [NSString stringWithUTF8String:artist], - MPMediaItemPropertyPlaybackDuration: @(trackDuration), - MPNowPlayingInfoPropertyElapsedPlaybackTime: @(elapsedTime) + MPMediaItemPropertyArtwork: coverArt }; infoCenter.nowPlayingInfo = nowPlayingInfo; +} + +/** + * C bridge setting the OS playback state to 'playing'. + */ +void set_os_playback_state_playing() { + MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter]; infoCenter.playbackState = MPNowPlayingPlaybackStatePlaying; } /** - * 'C' bridge function for 'setNowPlayingInfoNative' + * C bridge setting the OS playback state to 'paused'. */ -void setNowPlayingInfo(const char *title, const char *artist) { - const char *imagePath = "/path/to/image.png"; - double trackDuration = 300.0; - double elapsedTime = 120.0; - - setNowPlayingInfoNative(title, artist, trackDuration, NULL, elapsedTime); +void set_os_playback_state_paused() { + MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter]; + infoCenter.playbackState = MPNowPlayingPlaybackStatePaused; +} + +/** + * C bridge setting the OS playback state to 'stopped'. + */ +void set_os_playback_state_stopped() { + MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter]; + infoCenter.playbackState = MPNowPlayingPlaybackStateStopped; }