1 /*****************************************************************************
2 * osx_notifications.m : macOS notification plugin
4 * This plugin provides support for macOS notifications on current playlist
6 *****************************************************************************
7 * Copyright © 2008, 2011, 2012, 2015, 2018 the VideoLAN team
10 * Authors: Rafaël Carré <funman@videolanorg>
11 * Felix Paul Kühne <fkuehne@videolan.org
12 * Marvin Scholz <epirat07@gmail.com>
14 * This program is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation; either version 2 of the License, or
17 * (at your option) any later version.
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
29 #define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
35 #import <Foundation/Foundation.h>
36 #import <Cocoa/Cocoa.h>
38 #include <vlc_common.h>
39 #include <vlc_plugin.h>
40 #include <vlc_playlist_legacy.h>
41 #include <vlc_input.h>
43 #include <vlc_interface.h>
47 #pragma mark Class interfaces
48 @interface VLCNotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
50 /** Interface thread, required for skipping to the next item */
51 intf_thread_t * _Nonnull interfaceThread;
53 /** Holds the last notification so it can be cleared when the next one is delivered */
54 NSUserNotification * _Nullable lastNotification;
56 /** Indicates if VLC is in foreground */
61 * Initializes a new VLCNotification Delegate with a given intf_thread_t
63 - (instancetype)initWithInterfaceThread:(intf_thread_t * _Nonnull)intf_thread;
66 * Delegate method called when the current input changed
68 - (void)currentInputDidChanged:(input_thread_t * _Nonnull)input;
74 #pragma mark Local prototypes
77 void *vlcNotificationDelegate;
80 static int InputCurrent(vlc_object_t *, const char *,
81 vlc_value_t, vlc_value_t, void *);
85 #pragma mark C module functions
87 * Open: Initialization of the module
89 static int Open(vlc_object_t *p_this)
91 intf_thread_t *p_intf = (intf_thread_t *)p_this;
92 playlist_t *p_playlist = pl_Get(p_intf);
93 intf_sys_t *p_sys = p_intf->p_sys = calloc(1, sizeof(intf_sys_t));
99 VLCNotificationDelegate *notificationDelegate =
100 [[VLCNotificationDelegate alloc] initWithInterfaceThread:p_intf];
102 if (notificationDelegate == nil) {
107 p_sys->vlcNotificationDelegate = (__bridge_retained void*)notificationDelegate;
110 var_AddCallback(p_playlist, "input-current", InputCurrent, p_intf);
116 * Close: Destruction of the module
118 static void Close(vlc_object_t *p_this)
120 intf_thread_t *p_intf = (intf_thread_t *)p_this;
121 playlist_t *p_playlist = pl_Get(p_intf);
122 intf_sys_t *p_sys = p_intf->p_sys;
124 // Remove the callback, this must be done here, before deallocating the
125 // notification delegate object
126 var_DelCallback(p_playlist, "input-current", InputCurrent, p_intf);
129 // Transfer ownership of notification delegate object back to ARC
130 VLCNotificationDelegate *notificationDelegate =
131 (__bridge_transfer VLCNotificationDelegate*)p_sys->vlcNotificationDelegate;
133 // Ensure the object is deallocated
134 notificationDelegate = nil;
141 * Callback invoked on playlist item change
143 static int InputCurrent(vlc_object_t *p_this, const char *psz_var,
144 vlc_value_t oldval, vlc_value_t newval, void *param)
146 intf_thread_t *p_intf = (intf_thread_t *)param;
147 intf_sys_t *p_sys = p_intf->p_sys;
148 input_thread_t *p_input = newval.p_address;
152 VLCNotificationDelegate *notificationDelegate =
153 (__bridge VLCNotificationDelegate*)p_sys->vlcNotificationDelegate;
155 [notificationDelegate currentInputDidChanged:(input_thread_t *)p_input];
162 * Transfers a null-terminated UTF-8 C "string" to a NSString
163 * in a way that the NSString takes ownership of it.
165 * \warning After calling this function, passed cStr must not be used anymore!
167 * \param cStr Pointer to a zero-terminated UTF-8 encoded char array
169 * \return An NSString instance that uses cStr as internal data storage and
170 * frees it when done. On error, nil is returned and cStr is freed.
172 static inline NSString* CharsToNSString(char * _Nullable cStr)
177 NSString *resString = [[NSString alloc] initWithBytesNoCopy:cStr
179 encoding:NSUTF8StringEncoding
181 if (unlikely(resString == nil))
188 #pragma mark Class implementation
189 @implementation VLCNotificationDelegate
191 - (id)initWithInterfaceThread:(intf_thread_t *)intf_thread
196 interfaceThread = intf_thread;
198 // Subscribe to notifications to determine if VLC is in foreground or not
199 [[NSNotificationCenter defaultCenter] addObserver:self
200 selector:@selector(applicationActiveChange:)
201 name:NSApplicationDidBecomeActiveNotification
204 [[NSNotificationCenter defaultCenter] addObserver:self
205 selector:@selector(applicationActiveChange:)
206 name:NSApplicationDidResignActiveNotification
209 [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
215 - (void)currentInputDidChanged:(input_thread_t *)input
220 input_item_t *item = input_GetItem(input);
224 // Get title, first try now playing
225 NSString *title = CharsToNSString(input_item_GetNowPlayingFb(item));
227 // Fallback to item title or name
228 if ([title length] == 0)
229 title = CharsToNSString(input_item_GetTitleFbName(item));
231 // If there is still not title, do not notify
232 if (unlikely([title length] == 0))
236 NSString *artist = CharsToNSString(input_item_GetArtist(item));
239 NSString *album = CharsToNSString(input_item_GetAlbum(item));
242 NSString *artPath = nil;
244 char *psz_arturl = input_item_GetArtURL(item);
246 artPath = CharsToNSString(vlc_uri2path(psz_arturl));
250 // Construct final description string
251 NSString *desc = nil;
253 if (artist && album) {
254 desc = [NSString stringWithFormat:@"%@ – %@", artist, album];
260 [self notifyWithTitle:title description:desc imagePath:artPath];
264 * Called when the applications activity status changes
266 - (void)applicationActiveChange:(NSNotification *)n {
267 if (n.name == NSApplicationDidBecomeActiveNotification)
268 isInForeground = YES;
269 else if (n.name == NSApplicationDidResignActiveNotification)
274 * Called when the user interacts with a notification
276 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
277 didActivateNotification:(NSUserNotification *)notification
279 // Check if notification button ("Skip") was clicked
280 if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
282 playlist_Next(pl_Get(interfaceThread));
287 * Called when a new notification was delivered
289 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
290 didDeliverNotification:(NSUserNotification *)notification
292 // Only keep the most recent notification in the Notification Center
293 if (lastNotification)
294 [center removeDeliveredNotification:lastNotification];
296 lastNotification = notification;
300 * Send a notification to the default user notification center
302 - (void)notifyWithTitle:(NSString * _Nonnull)titleText
303 description:(NSString * _Nullable)descriptionText
304 imagePath:(NSString * _Nullable)imagePath
306 NSImage *image = nil;
310 image = [[NSImage alloc] initWithContentsOfFile:imagePath];
313 // Create notification
314 NSUserNotification *notification = [NSUserNotification new];
316 notification.title = titleText;
317 notification.subtitle = descriptionText;
318 notification.hasActionButton = YES;
319 notification.actionButtonTitle = [NSString stringWithUTF8String:_("Skip")];
321 // Try to set private properties
323 // Private API to set cover image, see rdar://23148801
324 [notification setValue:image forKey:@"_identityImage"];
325 // Private API to show action button, see rdar://23148733
326 [notification setValue:@(YES) forKey:@"_showsButtons"];
327 } @catch (NSException *exception) {
328 if (exception.name == NSUndefinedKeyException)
329 NSLog(@"VLC macOS notifcations plugin failed to set private notification values.");
335 [[NSUserNotificationCenter defaultUserNotificationCenter]
336 deliverNotification:notification];
344 [[NSNotificationCenter defaultCenter] removeObserver:self];
346 // Clear a remaining lastNotification in Notification Center, if any
347 if (lastNotification) {
348 [[NSUserNotificationCenter defaultUserNotificationCenter]
349 removeDeliveredNotification:lastNotification];
350 lastNotification = nil;
358 #pragma mark VLC Module descriptor
361 set_shortname("OSX-Notifications")
362 set_description(N_("macOS notifications plugin"))
363 add_shortcut("growl") // Kept for backwards compatibility
364 set_category(CAT_INTERFACE)
365 set_subcategory(SUBCAT_INTERFACE_CONTROL)
366 set_capability("interface", 0)
367 set_callbacks(Open, Close)