|
|
|
|
@@ -1,9 +1,266 @@
|
|
|
|
|
//go:build darwin
|
|
|
|
|
|
|
|
|
|
package sysmediacontrol
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
#cgo CFLAGS: -x objective-c
|
|
|
|
|
#cgo LDFLAGS: -framework Foundation -framework MediaPlayer -framework AppKit
|
|
|
|
|
|
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
|
#import <MediaPlayer/MediaPlayer.h>
|
|
|
|
|
#import <AppKit/AppKit.h>
|
|
|
|
|
|
|
|
|
|
// Forward declaration for Go export function
|
|
|
|
|
extern void handleCommand(int);
|
|
|
|
|
|
|
|
|
|
// Command handler
|
|
|
|
|
static MPRemoteCommandHandlerStatus commandHandler(MPRemoteCommandEvent *event, int command) {
|
|
|
|
|
handleCommand(command);
|
|
|
|
|
return MPRemoteCommandHandlerStatusSuccess;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize media player controls
|
|
|
|
|
static void initMediaPlayer() {
|
|
|
|
|
@autoreleasepool {
|
|
|
|
|
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
|
|
|
|
|
|
|
|
|
|
[[commandCenter playCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
|
|
|
|
return commandHandler(event, 0); // 0 = play
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
[[commandCenter pauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
|
|
|
|
return commandHandler(event, 1); // 1 = pause
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
[[commandCenter nextTrackCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
|
|
|
|
return commandHandler(event, 2); // 2 = next
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
[[commandCenter previousTrackCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
|
|
|
|
return commandHandler(event, 3); // 3 = previous
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
// Enable commands
|
|
|
|
|
[commandCenter playCommand].enabled = YES;
|
|
|
|
|
[commandCenter pauseCommand].enabled = YES;
|
|
|
|
|
[commandCenter nextTrackCommand].enabled = YES;
|
|
|
|
|
[commandCenter previousTrackCommand].enabled = YES;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update now playing info
|
|
|
|
|
static void updateNowPlaying(const char *title, const char *artist, const char *album,
|
|
|
|
|
double duration, double position, int isPlaying) {
|
|
|
|
|
@autoreleasepool {
|
|
|
|
|
MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
|
|
|
|
|
NSMutableDictionary *nowPlayingInfo = [center.nowPlayingInfo mutableCopy];
|
|
|
|
|
if (nowPlayingInfo == nil) {
|
|
|
|
|
nowPlayingInfo = [NSMutableDictionary dictionary];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (title != NULL) {
|
|
|
|
|
[nowPlayingInfo setObject:[NSString stringWithUTF8String:title]
|
|
|
|
|
forKey:MPMediaItemPropertyTitle];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (artist != NULL) {
|
|
|
|
|
[nowPlayingInfo setObject:[NSString stringWithUTF8String:artist]
|
|
|
|
|
forKey:MPMediaItemPropertyArtist];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (album != NULL) {
|
|
|
|
|
[nowPlayingInfo setObject:[NSString stringWithUTF8String:album]
|
|
|
|
|
forKey:MPMediaItemPropertyAlbumTitle];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (duration > 0) {
|
|
|
|
|
[nowPlayingInfo setObject:[NSNumber numberWithDouble:duration]
|
|
|
|
|
forKey:MPMediaItemPropertyPlaybackDuration];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (position >= 0) {
|
|
|
|
|
[nowPlayingInfo setObject:[NSNumber numberWithDouble:position]
|
|
|
|
|
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[nowPlayingInfo setObject:[NSNumber numberWithDouble:(isPlaying ? 1.0 : 0.0)]
|
|
|
|
|
forKey:MPNowPlayingInfoPropertyPlaybackRate];
|
|
|
|
|
|
|
|
|
|
center.nowPlayingInfo = nowPlayingInfo;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update artwork from URL
|
|
|
|
|
static void updateArtworkFromURL(const char *urlString) {
|
|
|
|
|
@autoreleasepool {
|
|
|
|
|
if (urlString == NULL) return;
|
|
|
|
|
|
|
|
|
|
NSString *urlStr = [NSString stringWithUTF8String:urlString];
|
|
|
|
|
NSURL *url = [NSURL URLWithString:urlStr];
|
|
|
|
|
if (url == NULL) return;
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
|
NSData *imageData = [NSData dataWithContentsOfURL:url];
|
|
|
|
|
if (imageData) {
|
|
|
|
|
NSImage *image = [[NSImage alloc] initWithData:imageData];
|
|
|
|
|
if (image) {
|
|
|
|
|
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc]
|
|
|
|
|
initWithBoundsSize:image.size
|
|
|
|
|
requestHandler:^NSImage * _Nonnull(CGSize size) {
|
|
|
|
|
return image;
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
|
|
|
|
|
NSMutableDictionary *nowPlayingInfo = [center.nowPlayingInfo mutableCopy];
|
|
|
|
|
if (nowPlayingInfo == nil) {
|
|
|
|
|
nowPlayingInfo = [NSMutableDictionary dictionary];
|
|
|
|
|
}
|
|
|
|
|
[nowPlayingInfo setObject:artwork forKey:MPMediaItemPropertyArtwork];
|
|
|
|
|
center.nowPlayingInfo = nowPlayingInfo;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear now playing info
|
|
|
|
|
static void clearNowPlaying() {
|
|
|
|
|
@autoreleasepool {
|
|
|
|
|
MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
|
|
|
|
|
center.nowPlayingInfo = nil;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
import "C"
|
|
|
|
|
import (
|
|
|
|
|
"AynaLivePlayer/core/events"
|
|
|
|
|
"AynaLivePlayer/global"
|
|
|
|
|
"AynaLivePlayer/pkg/eventbus"
|
|
|
|
|
"AynaLivePlayer/pkg/logger"
|
|
|
|
|
"unsafe"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
log logger.ILogger
|
|
|
|
|
currentTitle string
|
|
|
|
|
currentArtist string
|
|
|
|
|
currentAlbum string
|
|
|
|
|
currentDuration float64
|
|
|
|
|
currentPosition float64
|
|
|
|
|
currentIsPlaying bool
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//export handleCommand
|
|
|
|
|
func handleCommand(command C.int) {
|
|
|
|
|
switch command {
|
|
|
|
|
case 0: // Play
|
|
|
|
|
_ = global.EventBus.Publish(
|
|
|
|
|
events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: false})
|
|
|
|
|
case 1: // Pause
|
|
|
|
|
_ = global.EventBus.Publish(
|
|
|
|
|
events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: true})
|
|
|
|
|
case 2: // Next
|
|
|
|
|
_ = global.EventBus.Publish(
|
|
|
|
|
events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
|
|
|
|
|
case 3: // Previous
|
|
|
|
|
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
|
|
|
|
Position: 0,
|
|
|
|
|
Absolute: true,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateNowPlayingInfo() {
|
|
|
|
|
titleC := C.CString(currentTitle)
|
|
|
|
|
artistC := C.CString(currentArtist)
|
|
|
|
|
albumC := C.CString(currentAlbum)
|
|
|
|
|
defer C.free(unsafe.Pointer(titleC))
|
|
|
|
|
defer C.free(unsafe.Pointer(artistC))
|
|
|
|
|
defer C.free(unsafe.Pointer(albumC))
|
|
|
|
|
|
|
|
|
|
isPlaying := 0
|
|
|
|
|
if currentIsPlaying {
|
|
|
|
|
isPlaying = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
C.updateNowPlaying(
|
|
|
|
|
titleC,
|
|
|
|
|
artistC,
|
|
|
|
|
albumC,
|
|
|
|
|
C.double(currentDuration),
|
|
|
|
|
C.double(currentPosition),
|
|
|
|
|
C.int(isPlaying),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func InitSystemMediaControl() {
|
|
|
|
|
// stub
|
|
|
|
|
log = global.Logger.WithPrefix("SMTC-Darwin")
|
|
|
|
|
|
|
|
|
|
// Initialize media player controls
|
|
|
|
|
C.initMediaPlayer()
|
|
|
|
|
|
|
|
|
|
// Subscribe to player playing update events
|
|
|
|
|
global.EventBus.Subscribe("", events.PlayerPlayingUpdate, "sysmediacontrol.update_playing", func(event *eventbus.Event) {
|
|
|
|
|
data := event.Data.(events.PlayerPlayingUpdateEvent)
|
|
|
|
|
|
|
|
|
|
if data.Removed {
|
|
|
|
|
C.clearNowPlaying()
|
|
|
|
|
currentTitle = ""
|
|
|
|
|
currentArtist = ""
|
|
|
|
|
currentAlbum = ""
|
|
|
|
|
currentDuration = 0
|
|
|
|
|
currentPosition = 0
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentTitle = data.Media.Info.Title
|
|
|
|
|
currentArtist = data.Media.Info.Artist
|
|
|
|
|
currentAlbum = data.Media.Info.Album
|
|
|
|
|
|
|
|
|
|
updateNowPlayingInfo()
|
|
|
|
|
|
|
|
|
|
// Update artwork if available
|
|
|
|
|
if data.Media.Info.Cover.Url != "" {
|
|
|
|
|
urlC := C.CString(data.Media.Info.Cover.Url)
|
|
|
|
|
C.updateArtworkFromURL(urlC)
|
|
|
|
|
C.free(unsafe.Pointer(urlC))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Subscribe to pause state updates
|
|
|
|
|
global.EventBus.Subscribe("", events.PlayerPropertyPauseUpdate, "sysmediacontrol.update_paused", func(event *eventbus.Event) {
|
|
|
|
|
data := event.Data.(events.PlayerPropertyPauseUpdateEvent)
|
|
|
|
|
currentIsPlaying = !data.Paused
|
|
|
|
|
updateNowPlayingInfo()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Subscribe to duration updates
|
|
|
|
|
global.EventBus.Subscribe("", events.PlayerPropertyDurationUpdate, "sysmediacontrol.properties.duration", func(event *eventbus.Event) {
|
|
|
|
|
data := event.Data.(events.PlayerPropertyDurationUpdateEvent)
|
|
|
|
|
currentDuration = data.Duration
|
|
|
|
|
updateNowPlayingInfo()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Subscribe to time position updates
|
|
|
|
|
global.EventBus.Subscribe("", events.PlayerPropertyTimePosUpdate, "sysmediacontrol.properties.time_pos", func(event *eventbus.Event) {
|
|
|
|
|
data := event.Data.(events.PlayerPropertyTimePosUpdateEvent)
|
|
|
|
|
currentPosition = data.TimePos
|
|
|
|
|
updateNowPlayingInfo()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
log.Info("macOS System Media Control initialized")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Destroy() {
|
|
|
|
|
// stub
|
|
|
|
|
C.clearNowPlaying()
|
|
|
|
|
|
|
|
|
|
// Unsubscribe from all events
|
|
|
|
|
global.EventBus.Unsubscribe(events.PlayerPlayingUpdate, "sysmediacontrol.update_playing")
|
|
|
|
|
global.EventBus.Unsubscribe(events.PlayerPropertyPauseUpdate, "sysmediacontrol.update_paused")
|
|
|
|
|
global.EventBus.Unsubscribe(events.PlayerPropertyDurationUpdate, "sysmediacontrol.properties.duration")
|
|
|
|
|
global.EventBus.Unsubscribe(events.PlayerPropertyTimePosUpdate, "sysmediacontrol.properties.time_pos")
|
|
|
|
|
|
|
|
|
|
log.Info("macOS System Media Control destroyed")
|
|
|
|
|
}
|
|
|
|
|
|