|
| 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 | +} |
0 commit comments