Skip to content

Commit d71db57

Browse files
authored
mac-now-playing: Add new plugin
This plugin implements the Apple Now Playing interface. It allows Audacious to be controlled through media keys, to show the current album artwork and to seek the playback. See also: PR #193
1 parent 736e17e commit d71db57

File tree

7 files changed

+304
-0
lines changed

7 files changed

+304
-0
lines changed

configure.ac

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,19 @@ if test "x$enable_mac_media_keys" != "xno"; then
720720
GENERAL_PLUGINS="$GENERAL_PLUGINS mac-media-keys"
721721
fi
722722

723+
dnl Mac Now Playing
724+
dnl ============
725+
726+
AC_ARG_ENABLE(mac_now_playing,
727+
[AS_HELP_STRING([--disable-mac-now-playing], [disable Mac Now Playing (default=enabled)])],
728+
[enable_mac_now_playing=$enableval], [enable_mac_now_playing="yes"])
729+
730+
if test "x$enable_mac_now_playing" != "xno"; then
731+
if test "x$HAVE_DARWIN" != "xno"; then
732+
GENERAL_PLUGINS="$GENERAL_PLUGINS mac-now-playing"
733+
fi
734+
fi
735+
723736
dnl *** End of all plugin checks ***
724737

725738
plugindir=`pkg-config audacious --variable=plugin_dir`
@@ -918,5 +931,6 @@ if test "x$HAVE_DARWIN" = "xyes" ; then
918931
echo " -------------"
919932
echo " CoreAudio output: $have_coreaudio"
920933
echo " Media Keys: $enable_mac_media_keys"
934+
echo " Now Playing: $enable_mac_now_playing"
921935
echo
922936
fi

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ if meson.version().version_compare('>= 0.53')
352352
summary({
353353
'CoreAudio Output': get_option('coreaudio'),
354354
'Media Keys': get_option('mac-media-keys'),
355+
'Now Playing': get_option('mac-now-playing'),
355356
}, section: 'macOS Support')
356357
endif
357358
endif

meson_options.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ option('lirc', type: 'boolean', value: true,
9696
description: 'Whether the LIRC plugin is enabled')
9797
option('mac-media-keys', type: 'boolean', value: false,
9898
description: 'Whether the Mac Media Keys plugin is enabled')
99+
option('mac-now-playing', type: 'boolean', value: true,
100+
description: 'Whether the Mac Now Playing plugin is enabled')
99101
option('mpris2', type: 'boolean', value: true,
100102
description: 'Whether MPRIS 2.0 support is enabled')
101103
option('notify', type: 'boolean', value: true,
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*****************************************************************************/
2+
/* */
3+
/* Copyright (C) 2025 Yves Ndiaye */
4+
/* */
5+
/* This Source Code Form is subject to the terms of the Mozilla Public */
6+
/* License, v. 2.0. If a copy of the MPL was not distributed with this */
7+
/* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
8+
/* */
9+
/*****************************************************************************/
10+
11+
#import <Cocoa/Cocoa.h>
12+
#import <Foundation/Foundation.h>
13+
#import <MediaPlayer/MediaPlayer.h>
14+
#include <fstream>
15+
#include <iostream>
16+
#include <istream>
17+
18+
#include <libaudcore/drct.h>
19+
#include <libaudcore/hook.h>
20+
#include <libaudcore/i18n.h>
21+
#include <libaudcore/plugin.h>
22+
#include <libaudcore/probe.h>
23+
24+
class MacNowPlayingPlugin : public GeneralPlugin
25+
{
26+
public:
27+
static const char about[];
28+
static constexpr PluginInfo info = {N_("Mac Now Playing"), PACKAGE, about};
29+
30+
constexpr MacNowPlayingPlugin() : GeneralPlugin(info, true) {}
31+
32+
bool init();
33+
void cleanup();
34+
};
35+
36+
EXPORT MacNowPlayingPlugin aud_plugin_instance;
37+
38+
static void update_metadata(void * data, void * user);
39+
40+
static void update_player_time()
41+
{
42+
if (!aud_drct_get_ready())
43+
{
44+
return;
45+
}
46+
MPNowPlayingInfoCenter * center = [MPNowPlayingInfoCenter defaultCenter];
47+
double rate =
48+
[center playbackState] == MPNowPlayingPlaybackStatePlaying ? 1. : 0.;
49+
int current_time = aud_drct_get_time() / 1000;
50+
NSDictionary<NSString *, id> * info = [center nowPlayingInfo];
51+
NSMutableDictionary<NSString *, id> * minfo = [info mutableCopy];
52+
minfo[MPNowPlayingInfoPropertyPlaybackRate] =
53+
[[NSNumber alloc] initWithDouble:rate];
54+
minfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] =
55+
[[NSNumber alloc] initWithInt:current_time];
56+
center.nowPlayingInfo = minfo;
57+
}
58+
59+
static void remote_command_setup_handler()
60+
{
61+
// No need to change the now playing status since it will be cought by the
62+
// hook.
63+
MPRemoteCommandCenter * commandCenter =
64+
[MPRemoteCommandCenter sharedCommandCenter];
65+
// Play
66+
[[commandCenter playCommand] setEnabled:YES];
67+
[[commandCenter playCommand]
68+
addTargetWithHandler:^MPRemoteCommandHandlerStatus(
69+
MPRemoteCommandEvent * _Nonnull event) {
70+
aud_drct_play();
71+
return MPRemoteCommandHandlerStatusSuccess;
72+
}];
73+
// Pause
74+
[[commandCenter pauseCommand] setEnabled:YES];
75+
[[commandCenter pauseCommand]
76+
addTargetWithHandler:^MPRemoteCommandHandlerStatus(
77+
MPRemoteCommandEvent * _Nonnull event) {
78+
aud_drct_pause();
79+
return MPRemoteCommandHandlerStatusSuccess;
80+
}];
81+
// Toggle playpause
82+
[[commandCenter togglePlayPauseCommand] setEnabled:YES];
83+
[[commandCenter togglePlayPauseCommand]
84+
addTargetWithHandler:^MPRemoteCommandHandlerStatus(
85+
MPRemoteCommandEvent * _Nonnull event) {
86+
aud_drct_play_pause();
87+
return MPRemoteCommandHandlerStatusSuccess;
88+
}];
89+
// Skip forward
90+
[[commandCenter nextTrackCommand] setEnabled:YES];
91+
[[commandCenter nextTrackCommand]
92+
addTargetWithHandler:^MPRemoteCommandHandlerStatus(
93+
MPRemoteCommandEvent * _Nonnull event) {
94+
aud_drct_pl_next();
95+
return MPRemoteCommandHandlerStatusSuccess;
96+
}];
97+
// Skip backward
98+
[[commandCenter previousTrackCommand] setEnabled:YES];
99+
[[commandCenter previousTrackCommand]
100+
addTargetWithHandler:^MPRemoteCommandHandlerStatus(
101+
MPRemoteCommandEvent * _Nonnull event) {
102+
aud_drct_pl_prev();
103+
return MPRemoteCommandHandlerStatusSuccess;
104+
}];
105+
// Seek
106+
[[commandCenter changePlaybackPositionCommand] setEnabled:YES];
107+
[[commandCenter changePlaybackPositionCommand]
108+
addTargetWithHandler:^MPRemoteCommandHandlerStatus(
109+
MPRemoteCommandEvent * _Nonnull event) {
110+
if (aud_drct_get_playing())
111+
{
112+
MPChangePlaybackPositionCommandEvent * position_event =
113+
(MPChangePlaybackPositionCommandEvent *)event;
114+
// Apple time is in second
115+
aud_drct_seek((int)position_event.positionTime * 1000);
116+
}
117+
return MPRemoteCommandHandlerStatusSuccess;
118+
}];
119+
}
120+
121+
static void remote_command_disable_commands()
122+
{
123+
MPRemoteCommandCenter * commands =
124+
[MPRemoteCommandCenter sharedCommandCenter];
125+
commands.seekBackwardCommand.enabled = NO;
126+
commands.seekForwardCommand.enabled = NO;
127+
commands.changeRepeatModeCommand.enabled = NO;
128+
commands.changeShuffleModeCommand.enabled = NO;
129+
commands.skipBackwardCommand.enabled = NO;
130+
commands.enableLanguageOptionCommand.enabled = NO;
131+
commands.disableLanguageOptionCommand.enabled = NO;
132+
commands.ratingCommand.enabled = NO;
133+
commands.likeCommand.enabled = NO;
134+
commands.dislikeCommand.enabled = NO;
135+
commands.bookmarkCommand.enabled = NO;
136+
}
137+
138+
static void remote_command_set_playback_state()
139+
{
140+
MPNowPlayingInfoCenter * center = [MPNowPlayingInfoCenter defaultCenter];
141+
MPNowPlayingPlaybackState state = MPNowPlayingPlaybackStateStopped;
142+
if (aud_drct_get_playing())
143+
{
144+
bool paused = aud_drct_get_paused();
145+
state = paused ? MPNowPlayingPlaybackStatePaused
146+
: MPNowPlayingPlaybackStatePlaying;
147+
}
148+
[center setPlaybackState:state];
149+
}
150+
151+
static void remote_command_start()
152+
{
153+
MPNowPlayingInfoCenter * center = [MPNowPlayingInfoCenter defaultCenter];
154+
// Little hack to get the now playing starting otherwise Audacious
155+
// doesn't react to media keys
156+
[center setPlaybackState:MPNowPlayingPlaybackStatePaused];
157+
[center setPlaybackState:MPNowPlayingPlaybackStatePlaying];
158+
remote_command_set_playback_state();
159+
update_metadata(nullptr, nullptr);
160+
}
161+
162+
static void update_position(void * data, void * user) { update_player_time(); }
163+
164+
static void update_metadata(void * data, void * user)
165+
{
166+
MPNowPlayingInfoCenter * center = [MPNowPlayingInfoCenter defaultCenter];
167+
if (!aud_drct_get_ready())
168+
{
169+
return;
170+
}
171+
Tuple tuple = aud_drct_get_tuple();
172+
String title = tuple.get_str(Tuple::Title);
173+
String artist = tuple.get_str(Tuple::Artist);
174+
String album = tuple.get_str(Tuple::Album);
175+
String file = aud_drct_get_filename();
176+
int current_time = aud_drct_get_time();
177+
int length = tuple.get_int(Tuple::Length);
178+
179+
NSMutableDictionary<NSString *, id> * nowPlayingInfo =
180+
[[NSMutableDictionary alloc] init];
181+
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] =
182+
[[NSNumber alloc] initWithInt:MPNowPlayingInfoMediaTypeAudio];
183+
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] =
184+
[[NSNumber alloc] initWithBool:aud_drct_get_playing()];
185+
nowPlayingInfo[MPMediaItemPropertyTitle] =
186+
[[NSString alloc] initWithUTF8String:title ? title : ""];
187+
nowPlayingInfo[MPMediaItemPropertyArtist] =
188+
[[NSString alloc] initWithUTF8String:artist ? artist : ""];
189+
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] =
190+
[[NSString alloc] initWithUTF8String:album ? album : ""];
191+
nowPlayingInfo[MPMediaItemPropertyMediaType] =
192+
[[NSNumber alloc] initWithInt:MPMediaTypeMusic];
193+
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] =
194+
[[NSNumber alloc] initWithInt:length / 1000];
195+
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] =
196+
[[NSNumber alloc] initWithInt:current_time];
197+
198+
if (file)
199+
{
200+
AudArtPtr artwork = aud_art_request(file, AUD_ART_DATA);
201+
const Index<char> * artworkBytes = artwork.data();
202+
if (artworkBytes)
203+
{
204+
NSData * nsBytes =
205+
[[NSData alloc] initWithBytes:artworkBytes->begin()
206+
length:artworkBytes->len()];
207+
NSImage * image = [[NSImage alloc] initWithData:nsBytes];
208+
MPMediaItemArtwork * mpArtwork = [[MPMediaItemArtwork alloc]
209+
initWithBoundsSize:[image size]
210+
requestHandler:^NSImage * _Nonnull(CGSize size) {
211+
return image;
212+
}];
213+
nowPlayingInfo[MPMediaItemPropertyArtwork] = mpArtwork;
214+
}
215+
}
216+
[center setNowPlayingInfo:nowPlayingInfo];
217+
}
218+
219+
static void update_playback_status(void * data, void * user)
220+
{
221+
remote_command_set_playback_state();
222+
update_player_time();
223+
}
224+
225+
const char MacNowPlayingPlugin::about[] =
226+
N_("Now Playing Plugin for Audacious\n"
227+
"Copyright 2025 Yves Ndiaye\n\n"
228+
"This plugin implements the Apple Now Playing interface. "
229+
"It allows Audacious to be controlled through media keys, "
230+
"to show the current album artwork and to seek the playback.");
231+
232+
bool MacNowPlayingPlugin::init()
233+
{
234+
remote_command_setup_handler();
235+
remote_command_start();
236+
remote_command_disable_commands();
237+
238+
hook_associate("playback begin", update_playback_status, nullptr);
239+
hook_associate("playback pause", update_playback_status, nullptr);
240+
hook_associate("playback stop", update_playback_status, nullptr);
241+
hook_associate("playback unpause", update_playback_status, nullptr);
242+
hook_associate("playback ready", update_metadata, nullptr);
243+
hook_associate("playback stop", update_metadata, nullptr);
244+
hook_associate("tuple change", update_metadata, nullptr);
245+
hook_associate("playback ready", update_position, nullptr);
246+
hook_associate("playback seek", update_position, nullptr);
247+
return true;
248+
}
249+
250+
void MacNowPlayingPlugin::cleanup()
251+
{
252+
hook_dissociate("playback begin", update_playback_status);
253+
hook_dissociate("playback pause", update_playback_status);
254+
hook_dissociate("playback stop", update_playback_status);
255+
hook_dissociate("playback unpause", update_playback_status);
256+
hook_dissociate("playback ready", update_metadata);
257+
hook_dissociate("playback stop", update_metadata);
258+
hook_dissociate("tuple change", update_metadata);
259+
hook_dissociate("playback ready", update_position);
260+
hook_dissociate("playback seek", update_position);
261+
}

src/mac-now-playing/Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
PLUGIN = mac-now-playing${PLUGIN_SUFFIX}
2+
3+
SRCS = MacNowPlaying.mm
4+
5+
include ../../buildsys.mk
6+
include ../../extra.mk
7+
8+
plugindir := ${plugindir}/${GENERAL_PLUGIN_DIR}
9+
10+
LD = ${OBJCXX}
11+
12+
CFLAGS += ${PLUGIN_CFLAGS}
13+
CPPFLAGS += ${PLUGIN_CPPFLAGS} -I../..
14+
LIBS += -framework AppKit -framework MediaPlayer

src/mac-now-playing/meson.build

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
shared_module('mac-now-playing',
2+
['MacNowPlaying.mm'],
3+
dependencies: [audacious_dep],
4+
name_prefix: '',
5+
link_args: ['-framework', 'AppKit', '-framework', 'MediaPlayer'],
6+
install: true,
7+
install_dir: general_plugin_dir
8+
)

src/meson.build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ if get_option('mac-media-keys') and have_darwin
159159
subdir('mac-media-keys')
160160
endif
161161

162+
if get_option('mac-now-playing') and have_darwin
163+
subdir('mac-now-playing')
164+
endif
165+
162166
if get_option('mpris2') and not have_windows
163167
subdir('mpris2')
164168
endif

0 commit comments

Comments
 (0)