native functionality bridge
This commit is contained in:
+1
-59
@@ -1,70 +1,12 @@
|
|||||||
package backend
|
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 (
|
import (
|
||||||
"C"
|
|
||||||
)
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
|
||||||
"github.com/dweymouth/supersonic/player"
|
"github.com/dweymouth/supersonic/player"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MPMediaHandler is the handler for MacOS media controls and system events.
|
||||||
type MPMediaHandler struct {
|
type MPMediaHandler struct {
|
||||||
player *player.Player
|
player *player.Player
|
||||||
playbackManager *PlaybackManager
|
playbackManager *PlaybackManager
|
||||||
ArtURLLookup func(trackID string) (string, error)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+32
-4
@@ -1,18 +1,46 @@
|
|||||||
/**
|
/**
|
||||||
* mpmediabridge.h
|
* 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
|
* It offers a simplified interface to interact with the MPNowPlayingInfoCenter and other
|
||||||
* related media playback functionalities in macOS without dealing directly with
|
* related media playback functionalities in macOS without dealing directly with Objective-C code.
|
||||||
* Objective-C code.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <AppKit/AppKit.h>
|
#include <AppKit/AppKit.h>
|
||||||
#include <MediaPlayer/MediaPlayer.h>
|
#include <MediaPlayer/MediaPlayer.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Updates the "Now Playing" information on macOS for media playback
|
||||||
* using the MPNowPlayingInfoCenter API to set the metadata
|
* using the MPNowPlayingInfoCenter API to set the metadata
|
||||||
* for the currently playing media in the system's "Now Playing" interface.
|
* 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();
|
||||||
|
|||||||
+53
-17
@@ -1,47 +1,83 @@
|
|||||||
#import "mpmediabridge.h"
|
#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) {
|
void register_os_remote_commands() {
|
||||||
|
|
||||||
// TODO: the remove command center logic needs to move
|
|
||||||
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
|
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
|
||||||
[commandCenter.playCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
[commandCenter.playCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
||||||
// TODO: Handle play command
|
os_remote_command_callback(PLAY);
|
||||||
return MPRemoteCommandHandlerStatusSuccess;
|
return MPRemoteCommandHandlerStatusSuccess;
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[commandCenter.pauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
[commandCenter.pauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
||||||
// TODO: Handle pause command
|
os_remote_command_callback(PAUSE);
|
||||||
return MPRemoteCommandHandlerStatusSuccess;
|
return MPRemoteCommandHandlerStatusSuccess;
|
||||||
}];
|
}];
|
||||||
|
|
||||||
[commandCenter.togglePlayPauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
[commandCenter.togglePlayPauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
||||||
// TODO: Handle toggle command
|
os_remote_command_callback(TOGGLE);
|
||||||
return MPRemoteCommandHandlerStatusSuccess;
|
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 = @{
|
NSDictionary *nowPlayingInfo = @{
|
||||||
MPMediaItemPropertyTitle: [NSString stringWithUTF8String:title],
|
MPMediaItemPropertyTitle: [NSString stringWithUTF8String:title],
|
||||||
MPMediaItemPropertyArtist: [NSString stringWithUTF8String:artist],
|
MPMediaItemPropertyArtist: [NSString stringWithUTF8String:artist],
|
||||||
MPMediaItemPropertyPlaybackDuration: @(trackDuration),
|
MPMediaItemPropertyArtwork: coverArt
|
||||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: @(elapsedTime)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
infoCenter.nowPlayingInfo = nowPlayingInfo;
|
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;
|
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) {
|
void set_os_playback_state_paused() {
|
||||||
const char *imagePath = "/path/to/image.png";
|
MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter];
|
||||||
double trackDuration = 300.0;
|
infoCenter.playbackState = MPNowPlayingPlaybackStatePaused;
|
||||||
double elapsedTime = 120.0;
|
}
|
||||||
|
|
||||||
setNowPlayingInfoNative(title, artist, trackDuration, NULL, elapsedTime);
|
/**
|
||||||
|
* C bridge setting the OS playback state to 'stopped'.
|
||||||
|
*/
|
||||||
|
void set_os_playback_state_stopped() {
|
||||||
|
MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter];
|
||||||
|
infoCenter.playbackState = MPNowPlayingPlaybackStateStopped;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user