2 Commits

Author SHA1 Message Date
aynakeya
f59aebd2f8 add macos system media control 2025-10-19 01:12:19 +08:00
aynakeya
650da87f64 fix bili-video playlist 2025-10-08 21:14:29 +08:00
6 changed files with 267 additions and 8 deletions

View File

@@ -11,6 +11,7 @@ import (
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/pkg/logger"
loggerRepo "AynaLivePlayer/pkg/logger/repository"
"flag"
"os"

View File

@@ -32,7 +32,8 @@ func NewGuiContext(app fyne.App, mainWindow fyne.Window) *GuiContext {
func (c *GuiContext) Init() {
c.Window.SetOnClosed(func() {
for _, f := range c.onMainWindowClosing {
for idx, f := range c.onMainWindowClosing {
Logger.Debugf("runing gui closing handler #%d", idx)
f()
}
})

View File

@@ -12,7 +12,7 @@ func CreateView() fyne.CanvasObject {
gctx.Context.OnMainWindowClosing(func() {
if playerWindow != nil {
gctx.Logger.Infof("closing player window")
playerWindow.Close()
go playerWindow.Close()
}
})
return container.NewBorder(nil, createPlayControllerV2(), nil, nil, createPlaylist())

View File

@@ -16,8 +16,8 @@ func SetupSysTray() {
}))
desk.SetSystemTrayMenu(m)
desk.SetSystemTrayIcon(resource.ImageIcon)
gctx.Context.Window.SetCloseIntercept(func() {
gctx.Context.Window.Hide()
})
}
gctx.Context.Window.SetCloseIntercept(func() {
gctx.Context.Window.Hide()
})
}

View File

@@ -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")
}